keyshare.go 16.1 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
	URL                     string `json:"url"`
	Username                string `json:"username"`
	Nonce                   []byte `json:"nonce"`
58
59
	SchemeManagerIdentifier irma.SchemeManagerIdentifier
	token                   string
60
61
}

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

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

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

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

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

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

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

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

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

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

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

204
		// 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
205
206
207
208
209
		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 {
210
211
			irma.Logger.Info("Keyshare server token invalid, asking for PIN")
			irma.Logger.Debug("Token: ", ks.keyshareServer.token)
212
			ks.pinCheck = true
Sietse Ringers's avatar
Sietse Ringers committed
213
		}
214
215
		// Add a minute of leeway for possible clockdrift with the server,
		// and for the rest of the protocol to take place with this token
216
217
218
		if !claims.VerifyExpiresAt(time.Now().Add(1*time.Minute).Unix(), true) {
			irma.Logger.Info("Keyshare server token expires too soon, asking for PIN")
			irma.Logger.Debug("Token: ", ks.keyshareServer.token)
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
func verifyPinWorker(pin string, kss *keyshareServer, transport *irma.HTTPTransport) (
	success bool, tries int, blocked int, err error) {
	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:
		success = true
		kss.token = pinresult.Message
		transport.SetHeader(kssAuthHeader, kss.token)
		return
	case kssPinFailure:
		tries, err = strconv.Atoi(pinresult.Message)
		return
	case kssPinError:
		blocked, err = strconv.Atoi(pinresult.Message)
		return
	default:
304
305
306
307
308
		err = &irma.SessionError{
			Err:       errors.New("Keyshare server returned unrecognized PIN status"),
			ErrorType: irma.ErrorServerResponse,
			Info:      "Keyshare server returned unrecognized PIN status",
		}
309
310
311
312
		return
	}
}

Sietse Ringers's avatar
Sietse Ringers committed
313
314
315
316
317
318
319
320
// 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.
321
322
323
324
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
325
326
327
			continue
		}

328
329
		kss := ks.keyshareServers[manager]
		transport := ks.transports[manager]
330
331
		success, tries, blocked, err = verifyPinWorker(pin, kss, transport)
		if !success {
Sietse Ringers's avatar
Sietse Ringers committed
332
333
334
335
336
337
338
339
340
341
			return
		}
	}
	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() {
342
	pkids := map[irma.SchemeManagerIdentifier][]*publicKeyIdentifier{}
Sietse Ringers's avatar
Sietse Ringers committed
343
344
345
346
347
348
	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()
349
		managerID := irma.NewIssuerIdentifier(pk.Issuer).SchemeManagerIdentifier()
350
		if !ks.conf.SchemeManagers[managerID].Distributed() {
Sietse Ringers's avatar
Sietse Ringers committed
351
352
			continue
		}
353
354
		if _, contains := pkids[managerID]; !contains {
			pkids[managerID] = []*publicKeyIdentifier{}
Sietse Ringers's avatar
Sietse Ringers committed
355
		}
356
		pkids[managerID] = append(pkids[managerID], &publicKeyIdentifier{Issuer: pk.Issuer, Counter: pk.Counter})
Sietse Ringers's avatar
Sietse Ringers committed
357
358
359
360
	}

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

366
		transport := ks.transports[managerID]
Sietse Ringers's avatar
Sietse Ringers committed
367
		comms := &proofPCommitmentMap{}
368
		err := transport.Post("prove/getCommitments", comms, pkids[managerID])
Sietse Ringers's avatar
Sietse Ringers committed
369
		if err != nil {
370
371
372
373
374
375
376
377
			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
			}
378
			ks.sessionHandler.KeyshareError(&managerID, err)
Sietse Ringers's avatar
Sietse Ringers committed
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
			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() {
404
	_, issig := ks.session.(*irma.SignatureRequest)
405
	challenge := ks.builders.Challenge(ks.session.GetContext(), ks.session.GetNonce(), issig)
Sietse Ringers's avatar
Sietse Ringers committed
406
407

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

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

453
func (ks *keyshareSession) finishDisclosureOrSigning(challenge *big.Int, responses map[irma.SchemeManagerIdentifier]string) {
454
455
456
	proofPs := make([]*gabi.ProofP, len(ks.builders))
	for i, builder := range ks.builders {
		// Parse each received JWT
457
		managerID := irma.NewIssuerIdentifier(builder.PublicKey().Issuer).SchemeManagerIdentifier()
458
		if !ks.conf.SchemeManagers[managerID].Distributed() {
459
460
			continue
		}
461
462
		claims := struct {
			jwt.StandardClaims
463
464
			ProofP *gabi.ProofP
		}{}
465
466
467
		parser := new(jwt.Parser)
		parser.SkipClaimsValidation = true // no need to abort due to clock drift issues
		if _, err := parser.ParseWithClaims(responses[managerID], &claims, ks.conf.KeyshareServerKeyFunc(managerID)); err != nil {
468
			ks.sessionHandler.KeyshareError(&managerID, err)
469
470
			return
		}
471
		proofPs[i] = claims.ProofP
472
473
474
475
476
	}

	// Create merged proofs and finish protocol
	list, err := ks.builders.BuildDistributedProofList(challenge, proofPs)
	if err != nil {
477
		ks.sessionHandler.KeyshareError(nil, err)
478
479
480
481
		return
	}
	ks.sessionHandler.KeyshareDone(list)
}