keyshare.go 16 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

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

19
20
// 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
21
// (currently only Client).
22

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

type keyshareSessionHandler interface {
Sietse Ringers's avatar
Sietse Ringers committed
29
	KeyshareDone(message interface{})
Sietse Ringers's avatar
Sietse Ringers committed
30
	KeyshareCancelled()
31
	KeyshareBlocked(manager irma.SchemeManagerIdentifier, duration int)
Sietse Ringers's avatar
Sietse Ringers committed
32
	KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier)
33
	KeyshareEnrollmentDeleted(manager irma.SchemeManagerIdentifier)
34
35
	// 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
36
37
	KeysharePin()
	KeysharePinOK()
Sietse Ringers's avatar
Sietse Ringers committed
38
39
40
}

type keyshareSession struct {
41
42
43
44
45
46
47
48
49
	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
50
	pinCheck         bool
Sietse Ringers's avatar
Sietse Ringers committed
51
52
}

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

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

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

Sietse Ringers's avatar
Sietse Ringers committed
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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
113
114
115
116
117
118

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

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

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

func (ks *keyshareServer) HashedPin(pin string) string {
	hash := sha256.Sum256(append(ks.Nonce, []byte(pin)...))
146
147
148
149
	// 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"
150
}
Sietse Ringers's avatar
Sietse Ringers committed
151

Sietse Ringers's avatar
Sietse Ringers committed
152
153
154
155
156
157
158
159
// 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,
160
	builders gabi.ProofBuilderList,
161
	session irma.SessionRequest,
162
	conf *irma.Configuration,
163
	keyshareServers map[irma.SchemeManagerIdentifier]*keyshareServer,
164
	issuerProofNonce *big.Int,
Sietse Ringers's avatar
Sietse Ringers committed
165
166
) {
	ksscount := 0
167
	for managerID := range session.Identifiers().SchemeManagers {
168
		if conf.SchemeManagers[managerID].Distributed() {
Sietse Ringers's avatar
Sietse Ringers committed
169
			ksscount++
170
171
			if _, enrolled := keyshareServers[managerID]; !enrolled {
				err := errors.New("Not enrolled to keyshare server of scheme manager " + managerID.String())
172
				sessionHandler.KeyshareError(&managerID, err)
Sietse Ringers's avatar
Sietse Ringers committed
173
174
175
176
				return
			}
		}
	}
177
	if _, issuing := session.(*irma.IssuanceRequest); issuing && ksscount > 1 {
Sietse Ringers's avatar
Sietse Ringers committed
178
		err := errors.New("Issuance session involving more than one keyshare servers are not supported")
179
		sessionHandler.KeyshareError(nil, err)
Sietse Ringers's avatar
Sietse Ringers committed
180
181
182
183
		return
	}

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

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

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

207
208
209
210
211
212
		// 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
		parsed := &struct {
			Expiry irma.Timestamp `json:"exp"`
		}{}
		if err := irma.JwtDecode(ks.keyshareServer.token, parsed); err != nil {
			ks.pinCheck = true
Sietse Ringers's avatar
Sietse Ringers committed
213
		}
214
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
		if time.Time(parsed.Expiry).Before(time.Now().Add(1 * time.Minute)) {
			ks.pinCheck = true
Sietse Ringers's avatar
Sietse Ringers committed
218
219
220
		}
	}

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

229
230
231
func (ks *keyshareSession) fail(manager irma.SchemeManagerIdentifier, err error) {
	serr, ok := err.(*irma.SessionError)
	if ok {
Tomas's avatar
Tomas committed
232
233
		if serr.RemoteError != nil && len(serr.RemoteError.ErrorName) > 0 {
			switch serr.RemoteError.ErrorName {
234
235
			case "USER_NOT_FOUND":
				ks.sessionHandler.KeyshareEnrollmentDeleted(manager)
236
			case "USER_NOT_REGISTERED":
Sietse Ringers's avatar
Sietse Ringers committed
237
				ks.sessionHandler.KeyshareEnrollmentIncomplete(manager)
238
			case "USER_BLOCKED":
Tomas's avatar
Tomas committed
239
				duration, err := strconv.Atoi(serr.RemoteError.Message)
240
241
242
243
244
245
246
247
248
249
250
251
252
				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
253
254
255
// 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) {
256
	ks.pinRequestor.RequestPin(attempts, PinHandler(func(proceed bool, pin string) {
257
258
		if !proceed {
			ks.sessionHandler.KeyshareCancelled()
Sietse Ringers's avatar
Sietse Ringers committed
259
			return
260
		}
261
		success, attemptsRemaining, blocked, manager, err := ks.verifyPinAttempt(pin)
Sietse Ringers's avatar
Sietse Ringers committed
262
		if err != nil {
263
			ks.sessionHandler.KeyshareError(&manager, err)
Sietse Ringers's avatar
Sietse Ringers committed
264
265
266
			return
		}
		if blocked != 0 {
267
			ks.sessionHandler.KeyshareBlocked(manager, blocked)
Sietse Ringers's avatar
Sietse Ringers committed
268
269
270
			return
		}
		if success {
Sietse Ringers's avatar
Sietse Ringers committed
271
			ks.sessionHandler.KeysharePinOK()
Sietse Ringers's avatar
Sietse Ringers committed
272
273
274
275
276
			ks.GetCommitments()
			return
		}
		// Not successful but no error and not yet blocked: try again
		ks.VerifyPin(attemptsRemaining)
277
	}))
Sietse Ringers's avatar
Sietse Ringers committed
278
279
280
281
282
283
284
285
286
287
}

// 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.
288
289
290
291
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
292
293
294
			continue
		}

295
296
		kss := ks.keyshareServers[manager]
		transport := ks.transports[manager]
Sietse Ringers's avatar
Sietse Ringers committed
297
298
299
300
301
302
303
304
305
306
		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
307
			transport.SetHeader(kssAuthHeader, "Bearer "+kss.token)
Sietse Ringers's avatar
Sietse Ringers committed
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
		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() {
328
	pkids := map[irma.SchemeManagerIdentifier][]*publicKeyIdentifier{}
Sietse Ringers's avatar
Sietse Ringers committed
329
330
331
332
333
334
	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()
335
		managerID := irma.NewIssuerIdentifier(pk.Issuer).SchemeManagerIdentifier()
336
		if !ks.conf.SchemeManagers[managerID].Distributed() {
Sietse Ringers's avatar
Sietse Ringers committed
337
338
			continue
		}
339
340
		if _, contains := pkids[managerID]; !contains {
			pkids[managerID] = []*publicKeyIdentifier{}
Sietse Ringers's avatar
Sietse Ringers committed
341
		}
342
		pkids[managerID] = append(pkids[managerID], &publicKeyIdentifier{Issuer: pk.Issuer, Counter: pk.Counter})
Sietse Ringers's avatar
Sietse Ringers committed
343
344
345
346
	}

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

352
		transport := ks.transports[managerID]
Sietse Ringers's avatar
Sietse Ringers committed
353
		comms := &proofPCommitmentMap{}
354
		err := transport.Post("prove/getCommitments", comms, pkids[managerID])
Sietse Ringers's avatar
Sietse Ringers committed
355
		if err != nil {
356
357
358
359
360
361
362
363
			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
			}
364
			ks.sessionHandler.KeyshareError(&managerID, err)
Sietse Ringers's avatar
Sietse Ringers committed
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
			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() {
390
391
	_, issig := ks.session.(*irma.SignatureRequest)
	_, issuing := ks.session.(*irma.IssuanceRequest)
392
	challenge := ks.builders.Challenge(ks.session.GetContext(), ks.session.GetNonce(), issig)
Sietse Ringers's avatar
Sietse Ringers committed
393
394
395
396
397
398
	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 {
399
			ks.sessionHandler.KeyshareError(&ks.keyshareServer.SchemeManagerIdentifier, err)
Sietse Ringers's avatar
Sietse Ringers committed
400
401
402
403
404
		}
		kssChallenge = new(big.Int).SetBytes(bytes)
	}

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

	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.
426
func (ks *keyshareSession) Finish(challenge *big.Int, responses map[irma.SchemeManagerIdentifier]string) {
Sietse Ringers's avatar
Sietse Ringers committed
427
	switch ks.session.(type) {
428
	case *irma.DisclosureRequest: // Can't use fallthrough in a type switch in go
429
		ks.finishDisclosureOrSigning(challenge, responses)
430
	case *irma.SignatureRequest: // So we have to do this in a separate method
431
		ks.finishDisclosureOrSigning(challenge, responses)
432
	case *irma.IssuanceRequest:
Sietse Ringers's avatar
Sietse Ringers committed
433
434
435
		// 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
436
		list, err := ks.builders.BuildDistributedProofList(challenge, nil)
Sietse Ringers's avatar
Sietse Ringers committed
437
		if err != nil {
438
			ks.sessionHandler.KeyshareError(&ks.keyshareServer.SchemeManagerIdentifier, err)
Sietse Ringers's avatar
Sietse Ringers committed
439
440
			return
		}
441
		message := &gabi.IssueCommitmentMessage{Proofs: list, Nonce2: ks.issuerProofNonce}
Sietse Ringers's avatar
Sietse Ringers committed
442
443
444
		message.ProofPjwts = map[string]string{}
		for manager, response := range responses {
			message.ProofPjwts[manager.String()] = response
Sietse Ringers's avatar
Sietse Ringers committed
445
446
447
448
449
		}
		ks.sessionHandler.KeyshareDone(message)
	}
}

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

		// Decrypt the responses and populate a slice of ProofP's
		proofPs[i] = msg.ProofP
		bytes, err := ks.keyshareServer.PrivateKey.Decrypt(proofPs[i].SResponse.Bytes())
		if err != nil {
470
			ks.sessionHandler.KeyshareError(&managerID, err)
471
472
473
474
475
476
477
478
			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 {
479
		ks.sessionHandler.KeyshareError(nil, err)
480
481
482
483
		return
	}
	ks.sessionHandler.KeyshareDone(list)
}