keyshare.go 16.3 KB
Newer Older
1
package irmaclient
2
3
4
5
6

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
Sietse Ringers's avatar
Sietse Ringers committed
7
	"fmt"
8
	"net/http"
Sietse Ringers's avatar
Sietse Ringers committed
9
	"strconv"
Sietse Ringers's avatar
Sietse Ringers committed
10
	"strings"
11
	"time"
Sietse Ringers's avatar
Sietse Ringers committed
12

13
	"github.com/dgrijalva/jwt-go"
Sietse Ringers's avatar
Sietse Ringers committed
14
	"github.com/go-errors/errors"
15
16
	"github.com/privacybydesign/gabi"
	"github.com/privacybydesign/gabi/big"
17
	"github.com/privacybydesign/irmago"
18
19
)

20
21
// This file contains an implementation of the client side of the keyshare protocol,
// as well as the keyshareSessionHandler which is used to communicate with the user
22
// (currently only Client).
23

24
// KeysharePinRequestor is used to asking the user for his PIN.
Sietse Ringers's avatar
Sietse Ringers committed
25
type KeysharePinRequestor interface {
26
	RequestPin(remainingAttempts int, callback PinHandler)
Sietse Ringers's avatar
Sietse Ringers committed
27
28
29
}

type keyshareSessionHandler interface {
Sietse Ringers's avatar
Sietse Ringers committed
30
	KeyshareDone(message interface{})
Sietse Ringers's avatar
Sietse Ringers committed
31
	KeyshareCancelled()
32
	KeyshareBlocked(manager irma.SchemeManagerIdentifier, duration int)
Sietse Ringers's avatar
Sietse Ringers committed
33
	KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier)
34
	KeyshareEnrollmentDeleted(manager irma.SchemeManagerIdentifier)
35
36
	// In errors the manager may be nil, as not all keyshare errors have a clearly associated scheme manager
	KeyshareError(manager *irma.SchemeManagerIdentifier, err error)
Sietse Ringers's avatar
Sietse Ringers committed
37
38
	KeysharePin()
	KeysharePinOK()
Sietse Ringers's avatar
Sietse Ringers committed
39
40
41
}

type keyshareSession struct {
42
43
44
45
46
47
48
49
50
	sessionHandler   keyshareSessionHandler
	pinRequestor     KeysharePinRequestor
	builders         gabi.ProofBuilderList
	session          irma.SessionRequest
	conf             *irma.Configuration
	keyshareServers  map[irma.SchemeManagerIdentifier]*keyshareServer
	keyshareServer   *keyshareServer // The one keyshare server in use in case of issuance
	transports       map[irma.SchemeManagerIdentifier]*irma.HTTPTransport
	issuerProofNonce *big.Int
51
	pinCheck         bool
Sietse Ringers's avatar
Sietse Ringers committed
52
53
}

54
type keyshareServer struct {
55
56
57
58
59
60
	URL                     string              `json:"url"`
	Username                string              `json:"username"`
	Nonce                   []byte              `json:"nonce"`
	PrivateKey              *paillierPrivateKey `json:"keyPair"`
	SchemeManagerIdentifier irma.SchemeManagerIdentifier
	token                   string
61
62
}

63
type keyshareEnrollment struct {
Sietse Ringers's avatar
Sietse Ringers committed
64
65
	Username  string             `json:"username"`
	Pin       string             `json:"pin"`
Sietse Ringers's avatar
Sietse Ringers committed
66
	PublicKey *paillierPublicKey `json:"publicKey"`
67
	Email     *string            `json:"email"`
68
	Language  string             `json:"language"`
Sietse Ringers's avatar
Sietse Ringers committed
69
70
}

71
type keyshareChangepin struct {
72
	Username string `json:"id"`
73
74
	OldPin   string `json:"oldpin"`
	NewPin   string `json:"newpin"`
75
76
}

Sietse Ringers's avatar
Sietse Ringers committed
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
type keyshareAuthorization struct {
	Status     string   `json:"status"`
	Candidates []string `json:"candidates"`
}

type keysharePinMessage struct {
	Username string `json:"id"`
	Pin      string `json:"pin"`
}

type keysharePinStatus struct {
	Status  string `json:"status"`
	Message string `json:"message"`
}

type publicKeyIdentifier struct {
	Issuer  string `json:"issuer"`
	Counter uint   `json:"counter"`
}

Sietse Ringers's avatar
Sietse Ringers committed
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
func (pki *publicKeyIdentifier) UnmarshalText(text []byte) error {
	str := string(text)
	index := strings.LastIndex(str, "-")
	if index == -1 {
		return errors.New("Invalid publicKeyIdentifier")
	}
	counter, err := strconv.Atoi(str[index+1:])
	if err != nil {
		return err
	}
	*pki = publicKeyIdentifier{Issuer: str[:index], Counter: uint(counter)}
	return nil
}

func (pki *publicKeyIdentifier) MarshalText() (text []byte, err error) {
	return []byte(fmt.Sprintf("%s-%d", pki.Issuer, pki.Counter)), nil
}
Sietse Ringers's avatar
Sietse Ringers committed
114
115
116
117
118
119

type proofPCommitmentMap struct {
	Commitments map[publicKeyIdentifier]*gabi.ProofPCommitment `json:"c"`
}

const (
Sietse Ringers's avatar
Sietse Ringers committed
120
121
122
	kssUsernameHeader = "X-IRMA-Keyshare-Username"
	kssVersionHeader  = "X-IRMA-Keyshare-ProtocolVersion"
	kssAuthHeader     = "Authorization"
Sietse Ringers's avatar
Sietse Ringers committed
123
124
125
126
127
128
129
	kssAuthorized     = "authorized"
	kssTokenExpired   = "expired"
	kssPinSuccess     = "success"
	kssPinFailure     = "failure"
	kssPinError       = "error"
)

130
131
132
func newKeyshareServer(
	schemeManagerIdentifier irma.SchemeManagerIdentifier,
	privatekey *paillierPrivateKey,
133
	url string,
134
) (ks *keyshareServer, err error) {
Sietse Ringers's avatar
Sietse Ringers committed
135
	ks = &keyshareServer{
136
137
138
139
		Nonce:                   make([]byte, 32),
		URL:                     url,
		PrivateKey:              privatekey,
		SchemeManagerIdentifier: schemeManagerIdentifier,
Sietse Ringers's avatar
Sietse Ringers committed
140
	}
141
142
143
144
145
146
	_, err = rand.Read(ks.Nonce)
	return
}

func (ks *keyshareServer) HashedPin(pin string) string {
	hash := sha256.Sum256(append(ks.Nonce, []byte(pin)...))
147
148
149
150
	// We must be compatible with the old Android app here,
	// which uses Base64.encodeToString(hash, Base64.DEFAULT),
	// which appends a newline.
	return base64.StdEncoding.EncodeToString(hash[:]) + "\n"
151
}
Sietse Ringers's avatar
Sietse Ringers committed
152

Sietse Ringers's avatar
Sietse Ringers committed
153
154
155
156
157
158
159
160
// startKeyshareSession starts and completes the entire keyshare protocol with all involved keyshare servers
// for the specified session, merging the keyshare proofs into the specified ProofBuilder's.
// The user's pin is retrieved using the KeysharePinRequestor, repeatedly, until either it is correct; or the
// user cancels; or one of the keyshare servers blocks us.
// Error, blocked or success of the keyshare session is reported back to the keyshareSessionHandler.
func startKeyshareSession(
	sessionHandler keyshareSessionHandler,
	pin KeysharePinRequestor,
161
	builders gabi.ProofBuilderList,
162
	session irma.SessionRequest,
163
	conf *irma.Configuration,
164
	keyshareServers map[irma.SchemeManagerIdentifier]*keyshareServer,
165
	issuerProofNonce *big.Int,
Sietse Ringers's avatar
Sietse Ringers committed
166
167
) {
	ksscount := 0
168
	for managerID := range session.Identifiers().SchemeManagers {
169
		if conf.SchemeManagers[managerID].Distributed() {
Sietse Ringers's avatar
Sietse Ringers committed
170
			ksscount++
171
172
			if _, enrolled := keyshareServers[managerID]; !enrolled {
				err := errors.New("Not enrolled to keyshare server of scheme manager " + managerID.String())
173
				sessionHandler.KeyshareError(&managerID, err)
Sietse Ringers's avatar
Sietse Ringers committed
174
175
176
177
				return
			}
		}
	}
178
	if _, issuing := session.(*irma.IssuanceRequest); issuing && ksscount > 1 {
Sietse Ringers's avatar
Sietse Ringers committed
179
		err := errors.New("Issuance session involving more than one keyshare servers are not supported")
180
		sessionHandler.KeyshareError(nil, err)
Sietse Ringers's avatar
Sietse Ringers committed
181
182
183
184
		return
	}

	ks := &keyshareSession{
185
186
187
188
189
190
191
192
		session:          session,
		builders:         builders,
		sessionHandler:   sessionHandler,
		transports:       map[irma.SchemeManagerIdentifier]*irma.HTTPTransport{},
		pinRequestor:     pin,
		conf:             conf,
		keyshareServers:  keyshareServers,
		issuerProofNonce: issuerProofNonce,
193
		pinCheck:         false,
Sietse Ringers's avatar
Sietse Ringers committed
194
195
	}

196
	for managerID := range session.Identifiers().SchemeManagers {
197
		if !ks.conf.SchemeManagers[managerID].Distributed() {
Sietse Ringers's avatar
Sietse Ringers committed
198
199
200
			continue
		}

201
		ks.keyshareServer = ks.keyshareServers[managerID]
202
		transport := irma.NewHTTPTransport(ks.keyshareServer.URL)
Sietse Ringers's avatar
Sietse Ringers committed
203
		transport.SetHeader(kssUsernameHeader, ks.keyshareServer.Username)
Sietse Ringers's avatar
Sietse Ringers committed
204
205
		transport.SetHeader(kssAuthHeader, "Bearer "+ks.keyshareServer.token)
		transport.SetHeader(kssVersionHeader, "2")
206
		ks.transports[managerID] = transport
Sietse Ringers's avatar
Sietse Ringers committed
207

208
		// Try to parse token as a jwt to see if it is still valid; if so we don't need to ask for the PIN
209
210
211
212
213
		parser := new(jwt.Parser)
		parser.SkipClaimsValidation = true // We want to verify expiry on our own below so we can add leeway
		claims := jwt.StandardClaims{}
		_, err := parser.ParseWithClaims(ks.keyshareServer.token, &claims, ks.conf.KeyshareServerKeyFunc(managerID))
		if err != nil {
214
			ks.pinCheck = true
Sietse Ringers's avatar
Sietse Ringers committed
215
		}
216
217
		// Add a minute of leeway for possible clockdrift with the server,
		// and for the rest of the protocol to take place with this token
218
		if claims.VerifyExpiresAt(time.Now().Add(1*time.Minute).Unix(), true) {
219
			ks.pinCheck = true
Sietse Ringers's avatar
Sietse Ringers committed
220
221
222
		}
	}

223
	if ks.pinCheck {
Sietse Ringers's avatar
Sietse Ringers committed
224
		ks.sessionHandler.KeysharePin()
Sietse Ringers's avatar
Sietse Ringers committed
225
		ks.VerifyPin(-1)
226
227
	} else {
		ks.GetCommitments()
Sietse Ringers's avatar
Sietse Ringers committed
228
229
230
	}
}

231
232
233
func (ks *keyshareSession) fail(manager irma.SchemeManagerIdentifier, err error) {
	serr, ok := err.(*irma.SessionError)
	if ok {
Tomas's avatar
Tomas committed
234
235
		if serr.RemoteError != nil && len(serr.RemoteError.ErrorName) > 0 {
			switch serr.RemoteError.ErrorName {
236
237
			case "USER_NOT_FOUND":
				ks.sessionHandler.KeyshareEnrollmentDeleted(manager)
238
			case "USER_NOT_REGISTERED":
Sietse Ringers's avatar
Sietse Ringers committed
239
				ks.sessionHandler.KeyshareEnrollmentIncomplete(manager)
240
			case "USER_BLOCKED":
Tomas's avatar
Tomas committed
241
				duration, err := strconv.Atoi(serr.RemoteError.Message)
242
243
244
245
246
247
248
249
250
251
252
253
254
				if err != nil { // Not really clear what to do with duration, but should never happen anyway
					duration = -1
				}
				ks.sessionHandler.KeyshareBlocked(manager, duration)
			default:
				ks.sessionHandler.KeyshareError(&manager, err)
			}
		}
	} else {
		ks.sessionHandler.KeyshareError(&manager, err)
	}
}

Sietse Ringers's avatar
Sietse Ringers committed
255
256
257
// Ask for a pin, repeatedly if necessary, and either continue the keyshare protocol
// with authorization, or stop the keyshare protocol and inform of failure.
func (ks *keyshareSession) VerifyPin(attempts int) {
258
	ks.pinRequestor.RequestPin(attempts, PinHandler(func(proceed bool, pin string) {
259
260
		if !proceed {
			ks.sessionHandler.KeyshareCancelled()
Sietse Ringers's avatar
Sietse Ringers committed
261
			return
262
		}
263
		success, attemptsRemaining, blocked, manager, err := ks.verifyPinAttempt(pin)
Sietse Ringers's avatar
Sietse Ringers committed
264
		if err != nil {
265
			ks.sessionHandler.KeyshareError(&manager, err)
Sietse Ringers's avatar
Sietse Ringers committed
266
267
268
			return
		}
		if blocked != 0 {
269
			ks.sessionHandler.KeyshareBlocked(manager, blocked)
Sietse Ringers's avatar
Sietse Ringers committed
270
271
272
			return
		}
		if success {
Sietse Ringers's avatar
Sietse Ringers committed
273
			ks.sessionHandler.KeysharePinOK()
Sietse Ringers's avatar
Sietse Ringers committed
274
275
276
277
278
			ks.GetCommitments()
			return
		}
		// Not successful but no error and not yet blocked: try again
		ks.VerifyPin(attemptsRemaining)
279
	}))
Sietse Ringers's avatar
Sietse Ringers committed
280
281
282
283
284
285
286
287
288
289
}

// Verify the specified pin at each of the keyshare servers involved in the specified session.
// - If the pin did not verify at one of the keyshare servers but there are attempts remaining,
// the amount of remaining attempts is returned as the second return value.
// - If the pin did not verify at one of the keyshare servers and there are no attempts remaining,
// the amount of time for which we are blocked at the keyshare server is returned as the third
// parameter.
// - If this or anything else (specified in err) goes wrong, success will be false.
// If all is ok, success will be true.
290
291
292
293
func (ks *keyshareSession) verifyPinAttempt(pin string) (
	success bool, tries int, blocked int, manager irma.SchemeManagerIdentifier, err error) {
	for manager = range ks.session.Identifiers().SchemeManagers {
		if !ks.conf.SchemeManagers[manager].Distributed() {
Sietse Ringers's avatar
Sietse Ringers committed
294
295
296
			continue
		}

297
298
		kss := ks.keyshareServers[manager]
		transport := ks.transports[manager]
Sietse Ringers's avatar
Sietse Ringers committed
299
300
301
302
303
304
305
306
307
308
		pinmsg := keysharePinMessage{Username: kss.Username, Pin: kss.HashedPin(pin)}
		pinresult := &keysharePinStatus{}
		err = transport.Post("users/verify/pin", pinresult, pinmsg)
		if err != nil {
			return
		}

		switch pinresult.Status {
		case kssPinSuccess:
			kss.token = pinresult.Message
Sietse Ringers's avatar
Sietse Ringers committed
309
			transport.SetHeader(kssAuthHeader, "Bearer "+kss.token)
Sietse Ringers's avatar
Sietse Ringers committed
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
		case kssPinFailure:
			tries, err = strconv.Atoi(pinresult.Message)
			return
		case kssPinError:
			blocked, err = strconv.Atoi(pinresult.Message)
			return
		default:
			err = errors.New("Keyshare server returned unrecognized PIN status")
			return
		}
	}

	success = true
	return
}

// GetCommitments gets the commitments (first message in Schnorr zero-knowledge protocol)
// of all keyshare servers of their part of the private key, and merges these commitments
// in our own proof builders.
func (ks *keyshareSession) GetCommitments() {
330
	pkids := map[irma.SchemeManagerIdentifier][]*publicKeyIdentifier{}
Sietse Ringers's avatar
Sietse Ringers committed
331
332
333
334
335
336
	commitments := map[publicKeyIdentifier]*gabi.ProofPCommitment{}

	// For each scheme manager, build a list of public keys under this manager
	// that we will use in the keyshare protocol with the keyshare server of this manager
	for _, builder := range ks.builders {
		pk := builder.PublicKey()
337
		managerID := irma.NewIssuerIdentifier(pk.Issuer).SchemeManagerIdentifier()
338
		if !ks.conf.SchemeManagers[managerID].Distributed() {
Sietse Ringers's avatar
Sietse Ringers committed
339
340
			continue
		}
341
342
		if _, contains := pkids[managerID]; !contains {
			pkids[managerID] = []*publicKeyIdentifier{}
Sietse Ringers's avatar
Sietse Ringers committed
343
		}
344
		pkids[managerID] = append(pkids[managerID], &publicKeyIdentifier{Issuer: pk.Issuer, Counter: pk.Counter})
Sietse Ringers's avatar
Sietse Ringers committed
345
346
347
348
	}

	// Now inform each keyshare server of with respect to which public keys
	// we want them to send us commitments
349
	for managerID := range ks.session.Identifiers().SchemeManagers {
350
		if !ks.conf.SchemeManagers[managerID].Distributed() {
Sietse Ringers's avatar
Sietse Ringers committed
351
352
353
			continue
		}

354
		transport := ks.transports[managerID]
Sietse Ringers's avatar
Sietse Ringers committed
355
		comms := &proofPCommitmentMap{}
356
		err := transport.Post("prove/getCommitments", comms, pkids[managerID])
Sietse Ringers's avatar
Sietse Ringers committed
357
		if err != nil {
358
359
360
361
362
363
364
365
			if err.(*irma.SessionError).RemoteError.Status == http.StatusForbidden && !ks.pinCheck {
				// JWT may be out of date due to clock drift; request pin and try again
				// (but only if we did not ask for a PIN earlier)
				ks.pinCheck = false
				ks.sessionHandler.KeysharePin()
				ks.VerifyPin(-1)
				return
			}
366
			ks.sessionHandler.KeyshareError(&managerID, err)
Sietse Ringers's avatar
Sietse Ringers committed
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
			return
		}
		for pki, c := range comms.Commitments {
			commitments[pki] = c
		}
	}

	// Merge in the commitments
	for _, builder := range ks.builders {
		pk := builder.PublicKey()
		pki := publicKeyIdentifier{Issuer: pk.Issuer, Counter: pk.Counter}
		comm, distributed := commitments[pki]
		if !distributed {
			continue
		}
		builder.MergeProofPCommitment(comm)
	}

	ks.GetProofPs()
}

// GetProofPs uses the combined commitments of all keyshare servers and ourself
// to calculate the challenge, which is sent to the keyshare servers in order to
// receive their responses (2nd and 3rd message in Schnorr zero-knowledge protocol).
func (ks *keyshareSession) GetProofPs() {
392
393
	_, issig := ks.session.(*irma.SignatureRequest)
	_, issuing := ks.session.(*irma.IssuanceRequest)
394
	challenge := ks.builders.Challenge(ks.session.GetContext(), ks.session.GetNonce(), issig)
Sietse Ringers's avatar
Sietse Ringers committed
395
396
397
398
399
400
	kssChallenge := challenge

	// In disclosure or signature sessions the challenge is Paillier encrypted.
	if !issuing {
		bytes, err := ks.keyshareServer.PrivateKey.Encrypt(challenge.Bytes())
		if err != nil {
401
			ks.sessionHandler.KeyshareError(&ks.keyshareServer.SchemeManagerIdentifier, err)
Sietse Ringers's avatar
Sietse Ringers committed
402
403
404
405
406
		}
		kssChallenge = new(big.Int).SetBytes(bytes)
	}

	// Post the challenge, obtaining JWT's containing the ProofP's
407
	responses := map[irma.SchemeManagerIdentifier]string{}
408
	for managerID := range ks.session.Identifiers().SchemeManagers {
409
		transport, distributed := ks.transports[managerID]
Sietse Ringers's avatar
Sietse Ringers committed
410
411
412
413
414
415
		if !distributed {
			continue
		}
		var jwt string
		err := transport.Post("prove/getResponse", &jwt, kssChallenge)
		if err != nil {
416
			ks.sessionHandler.KeyshareError(&managerID, err)
Sietse Ringers's avatar
Sietse Ringers committed
417
418
			return
		}
419
		responses[managerID] = jwt
Sietse Ringers's avatar
Sietse Ringers committed
420
421
422
423
424
425
426
427
	}

	ks.Finish(challenge, responses)
}

// Finish the keyshare protocol: in case of issuance, put the keyshare jwt in the
// IssueCommitmentMessage; in case of disclosure and signing, parse each keyshare jwt,
// merge in the received ProofP's, and finish.
428
func (ks *keyshareSession) Finish(challenge *big.Int, responses map[irma.SchemeManagerIdentifier]string) {
Sietse Ringers's avatar
Sietse Ringers committed
429
	switch ks.session.(type) {
430
	case *irma.DisclosureRequest: // Can't use fallthrough in a type switch in go
431
		ks.finishDisclosureOrSigning(challenge, responses)
432
	case *irma.SignatureRequest: // So we have to do this in a separate method
433
		ks.finishDisclosureOrSigning(challenge, responses)
434
	case *irma.IssuanceRequest:
Sietse Ringers's avatar
Sietse Ringers committed
435
436
437
		// Calculate IssueCommitmentMessage, without merging in any of the received ProofP's:
		// instead, include the keyshare server's JWT in the IssueCommitmentMessage for the
		// issuance server to verify
438
		list, err := ks.builders.BuildDistributedProofList(challenge, nil)
Sietse Ringers's avatar
Sietse Ringers committed
439
		if err != nil {
440
			ks.sessionHandler.KeyshareError(&ks.keyshareServer.SchemeManagerIdentifier, err)
Sietse Ringers's avatar
Sietse Ringers committed
441
442
			return
		}
443
		message := &gabi.IssueCommitmentMessage{Proofs: list, Nonce2: ks.issuerProofNonce}
Sietse Ringers's avatar
Sietse Ringers committed
444
445
446
		message.ProofPjwts = map[string]string{}
		for manager, response := range responses {
			message.ProofPjwts[manager.String()] = response
Sietse Ringers's avatar
Sietse Ringers committed
447
448
449
450
451
		}
		ks.sessionHandler.KeyshareDone(message)
	}
}

452
func (ks *keyshareSession) finishDisclosureOrSigning(challenge *big.Int, responses map[irma.SchemeManagerIdentifier]string) {
453
454
455
	proofPs := make([]*gabi.ProofP, len(ks.builders))
	for i, builder := range ks.builders {
		// Parse each received JWT
456
		managerID := irma.NewIssuerIdentifier(builder.PublicKey().Issuer).SchemeManagerIdentifier()
457
		if !ks.conf.SchemeManagers[managerID].Distributed() {
458
459
			continue
		}
460
461
		claims := struct {
			jwt.StandardClaims
462
463
			ProofP *gabi.ProofP
		}{}
464
		if _, err := jwt.ParseWithClaims(responses[managerID], &claims, ks.conf.KeyshareServerKeyFunc(managerID)); err != nil {
465
			ks.sessionHandler.KeyshareError(&managerID, err)
466
467
468
469
			return
		}

		// Decrypt the responses and populate a slice of ProofP's
470
		proofPs[i] = claims.ProofP
471
472
		bytes, err := ks.keyshareServer.PrivateKey.Decrypt(proofPs[i].SResponse.Bytes())
		if err != nil {
473
			ks.sessionHandler.KeyshareError(&managerID, err)
474
475
476
477
478
479
480
481
			return
		}
		proofPs[i].SResponse = new(big.Int).SetBytes(bytes)
	}

	// Create merged proofs and finish protocol
	list, err := ks.builders.BuildDistributedProofList(challenge, proofPs)
	if err != nil {
482
		ks.sessionHandler.KeyshareError(nil, err)
483
484
485
486
		return
	}
	ks.sessionHandler.KeyshareDone(list)
}