session.go 14.7 KB
Newer Older
1
package irmaclient
2
3

import (
Sietse Ringers's avatar
Sietse Ringers committed
4
5
	"fmt"
	"sort"
6
	"strconv"
7
8
	"strings"

9
10
11
	"math/big"

	"github.com/credentials/irmago"
12
	"github.com/go-errors/errors"
13
	"github.com/mhe/gabi"
14
15
)

Sietse Ringers's avatar
Sietse Ringers committed
16
17
18
// This file contains the client side of the IRMA protocol, as well as the Handler interface
// which is used to communicate session info with the user.

Sietse Ringers's avatar
Sietse Ringers committed
19
20
// PermissionHandler is a callback for providing permission for an IRMA session
// and specifying the attributes to be disclosed.
21
type PermissionHandler func(proceed bool, choice *irma.DisclosureChoice)
22

23
24
type PinHandler func(proceed bool, pin string)

25
26
// A Handler contains callbacks for communication to the user.
type Handler interface {
27
28
29
30
31
32
33
34
35
36
37
	StatusUpdate(action irma.Action, status irma.Status)
	Success(action irma.Action)
	Cancelled(action irma.Action)
	Failure(action irma.Action, err *irma.SessionError)
	UnsatisfiableRequest(action irma.Action, missing irma.AttributeDisjunctionList)
	MissingKeyshareEnrollment(manager irma.SchemeManagerIdentifier)

	RequestIssuancePermission(request irma.IssuanceRequest, ServerName string, callback PermissionHandler)
	RequestVerificationPermission(request irma.DisclosureRequest, ServerName string, callback PermissionHandler)
	RequestSignaturePermission(request irma.SignatureRequest, ServerName string, callback PermissionHandler)
	RequestSchemeManagerPermission(manager *irma.SchemeManager, callback func(proceed bool))
Sietse Ringers's avatar
Sietse Ringers committed
38

39
	RequestPin(remainingAttempts int, callback PinHandler)
40
41
}

42
43
44
45
type SessionDismisser interface {
	Dismiss()
}

Sietse Ringers's avatar
Sietse Ringers committed
46
47
// A session is an IRMA session.
type session struct {
48
49
	Action    irma.Action
	Version   irma.Version
50
51
52
	ServerURL string
	Handler   Handler

53
	info        *irma.SessionInfo
54
	client      *Client
55
56
57
58
59
	jwt         irma.RequestorJwt
	irmaSession irma.IrmaSession
	transport   *irma.HTTPTransport
	choice      *irma.DisclosureChoice
	downloaded  *irma.IrmaIdentifierSet
60
	done        bool
61
62
}

63
64
65
// We implement the handler for the keyshare protocol
var _ keyshareSessionHandler = (*session)(nil)

66
67
// Supported protocol versions. Minor version numbers should be reverse sorted.
var supportedVersions = map[int][]int{
68
	2: {2, 1},
69
70
}

71
func calcVersion(qr *irma.Qr) (string, error) {
72
	// Parse range supported by server
73
74
75
76
77
78
79
80
81
82
83
84
	var minmajor, minminor, maxmajor, maxminor int
	var err error
	if minmajor, err = strconv.Atoi(string(qr.ProtocolVersion[0])); err != nil {
		return "", err
	}
	if minminor, err = strconv.Atoi(string(qr.ProtocolVersion[2])); err != nil {
		return "", err
	}
	if maxmajor, err = strconv.Atoi(string(qr.ProtocolMaxVersion[0])); err != nil {
		return "", err
	}
	if maxminor, err = strconv.Atoi(string(qr.ProtocolMaxVersion[2])); err != nil {
85
86
87
88
89
		return "", err
	}

	// Iterate supportedVersions in reverse sorted order (i.e. biggest major number first)
	keys := make([]int, 0, len(supportedVersions))
90
	for k := range supportedVersions {
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
		keys = append(keys, k)
	}
	sort.Sort(sort.Reverse(sort.IntSlice(keys)))
	for _, major := range keys {
		for _, minor := range supportedVersions[major] {
			aboveMinimum := major > minmajor || (major == minmajor && minor >= minminor)
			underMaximum := major < maxmajor || (major == maxmajor && minor <= maxminor)
			if aboveMinimum && underMaximum {
				return fmt.Sprintf("%d.%d", major, minor), nil
			}
		}
	}
	return "", fmt.Errorf("No supported protocol version between %s and %s", qr.ProtocolVersion, qr.ProtocolMaxVersion)
}

106
// NewSession creates and starts a new IRMA session.
107
func (client *Client) NewSession(qr *irma.Qr, handler Handler) SessionDismisser {
Sietse Ringers's avatar
Sietse Ringers committed
108
	session := &session{
109
		Action:    irma.Action(qr.Type),
110
111
		ServerURL: qr.URL,
		Handler:   handler,
112
		transport: irma.NewHTTPTransport(qr.URL),
113
		client:    client,
114
	}
115
116
117
118
119
120

	if session.Action == irma.ActionSchemeManager {
		go session.managerSession()
		return session
	}

121
122
	version, err := calcVersion(qr)
	if err != nil {
123
		session.fail(&irma.SessionError{ErrorType: irma.ErrorProtocolVersionNotSupported, Err: err})
124
		return nil
125
	}
126
	session.Version = irma.Version(version)
127
128
129

	// Check if the action is one of the supported types
	switch session.Action {
130
131
132
133
	case irma.ActionDisclosing: // nop
	case irma.ActionSigning: // nop
	case irma.ActionIssuing: // nop
	case irma.ActionUnknown:
134
135
		fallthrough
	default:
136
		session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownAction, Info: string(session.Action)})
137
		return nil
138
139
140
141
142
143
144
145
	}

	if !strings.HasSuffix(session.ServerURL, "/") {
		session.ServerURL += "/"
	}

	go session.start()

146
	return session
147
148
}

149
150
// start retrieves the first message in the IRMA protocol, checks if we can perform
// the request, and informs the user of the outcome.
Sietse Ringers's avatar
Sietse Ringers committed
151
func (session *session) start() {
152
	defer func() {
153
		handlePanic(func(err *irma.SessionError) {
154
155
156
157
158
159
			if session.Handler != nil {
				session.Handler.Failure(session.Action, err)
			}
		})
	}()

160
	session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating)
161

Sietse Ringers's avatar
Sietse Ringers committed
162
	// Get the first IRMA protocol message and parse it
163
	session.info = &irma.SessionInfo{}
Sietse Ringers's avatar
Sietse Ringers committed
164
	Err := session.transport.Get("jwt", session.info)
Sietse Ringers's avatar
Sietse Ringers committed
165
	if Err != nil {
166
		session.fail(Err.(*irma.SessionError))
167
168
169
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
170
	var err error
171
	session.jwt, err = irma.ParseRequestorJwt(session.Action, session.info.Jwt)
Sietse Ringers's avatar
Sietse Ringers committed
172
	if err != nil {
173
		session.fail(&irma.SessionError{ErrorType: irma.ErrorInvalidJWT, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
174
175
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
176
	session.irmaSession = session.jwt.IrmaSession()
Sietse Ringers's avatar
Sietse Ringers committed
177
178
	session.irmaSession.SetContext(session.info.Context)
	session.irmaSession.SetNonce(session.info.Nonce)
179
180
	if session.Action == irma.ActionIssuing {
		ir := session.irmaSession.(*irma.IssuanceRequest)
181
		// Store which public keys the server will use
182
		for _, credreq := range ir.Credentials {
183
			credreq.KeyCounter = session.info.Keys[credreq.CredentialTypeID.IssuerIdentifier()]
184
185
186
		}
	}

187
	// Check if we are enrolled into all involved keyshare servers
Sietse Ringers's avatar
Sietse Ringers committed
188
	for id := range session.irmaSession.Identifiers().SchemeManagers {
189
		manager, ok := session.client.Configuration.SchemeManagers[id]
190
		if !ok {
191
			session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownSchemeManager, Info: id.String()})
192
193
194
			return
		}
		distributed := manager.Distributed()
195
		_, enrolled := session.client.keyshareServers[id]
196
		if distributed && !enrolled {
197
			session.delete()
198
			session.Handler.MissingKeyshareEnrollment(id)
Sietse Ringers's avatar
Sietse Ringers committed
199
200
201
202
			return
		}
	}

Sietse Ringers's avatar
Sietse Ringers committed
203
	// Download missing credential types/issuers/public keys from the scheme manager
204
	if session.downloaded, err = session.client.Configuration.Download(session.irmaSession.Identifiers()); err != nil {
205
206
		session.Handler.Failure(
			session.Action,
207
			&irma.SessionError{ErrorType: irma.ErrorConfigurationDownload, Err: err},
208
209
210
211
		)
		return
	}

212
213
	if session.Action == irma.ActionIssuing {
		ir := session.irmaSession.(*irma.IssuanceRequest)
Sietse Ringers's avatar
Sietse Ringers committed
214
		for _, credreq := range ir.Credentials {
215
			info, err := credreq.Info(session.client.Configuration)
216
			if err != nil {
217
				session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownCredentialType, Err: err})
218
219
220
				return
			}
			ir.CredentialInfoList = append(ir.CredentialInfoList, info)
221
222
223
		}
	}

224
	candidates, missing := session.client.CheckSatisfiability(session.irmaSession.ToDisclose())
225
226
	if len(missing) > 0 {
		session.Handler.UnsatisfiableRequest(session.Action, missing)
227
		// TODO: session.transport.Delete() on dialog cancel
228
229
		return
	}
230
	session.irmaSession.SetCandidates(candidates)
231

Sietse Ringers's avatar
Sietse Ringers committed
232
	// Ask for permission to execute the session
233
	callback := PermissionHandler(func(proceed bool, choice *irma.DisclosureChoice) {
Sietse Ringers's avatar
Sietse Ringers committed
234
235
236
		session.choice = choice
		session.irmaSession.SetDisclosureChoice(choice)
		go session.do(proceed)
237
	})
238
	session.Handler.StatusUpdate(session.Action, irma.StatusConnected)
239
	switch session.Action {
240
	case irma.ActionDisclosing:
241
		session.Handler.RequestVerificationPermission(
242
243
			*session.irmaSession.(*irma.DisclosureRequest), session.jwt.Requestor(), callback)
	case irma.ActionSigning:
244
		session.Handler.RequestSignaturePermission(
245
246
			*session.irmaSession.(*irma.SignatureRequest), session.jwt.Requestor(), callback)
	case irma.ActionIssuing:
247
		session.Handler.RequestIssuancePermission(
248
			*session.irmaSession.(*irma.IssuanceRequest), session.jwt.Requestor(), callback)
249
250
251
252
253
	default:
		panic("Invalid session type") // does not happen, session.Action has been checked earlier
	}
}

Sietse Ringers's avatar
Sietse Ringers committed
254
func (session *session) do(proceed bool) {
255
	defer func() {
256
		handlePanic(func(err *irma.SessionError) {
257
258
259
260
261
262
			if session.Handler != nil {
				session.Handler.Failure(session.Action, err)
			}
		})
	}()

263
	if !proceed {
264
		session.cancel()
265
266
		return
	}
267
	session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating)
268

269
	if !session.irmaSession.Identifiers().Distributed(session.client.Configuration) {
Sietse Ringers's avatar
Sietse Ringers committed
270
271
272
		var message interface{}
		var err error
		switch session.Action {
273
		case irma.ActionSigning:
274
			message, err = session.client.Proofs(session.choice, session.irmaSession, true)
275
		case irma.ActionDisclosing:
276
			message, err = session.client.Proofs(session.choice, session.irmaSession, false)
277
278
		case irma.ActionIssuing:
			message, err = session.client.IssueCommitments(session.irmaSession.(*irma.IssuanceRequest))
Sietse Ringers's avatar
Sietse Ringers committed
279
280
		}
		if err != nil {
281
			session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
282
283
284
285
			return
		}
		session.sendResponse(message)
	} else {
286
		var builders gabi.ProofBuilderList
Sietse Ringers's avatar
Sietse Ringers committed
287
288
		var err error
		switch session.Action {
289
		case irma.ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
290
			fallthrough
291
		case irma.ActionDisclosing:
292
			builders, err = session.client.ProofBuilders(session.choice)
293
294
		case irma.ActionIssuing:
			builders, err = session.client.IssuanceProofBuilders(session.irmaSession.(*irma.IssuanceRequest))
Sietse Ringers's avatar
Sietse Ringers committed
295
296
		}
		if err != nil {
297
			session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
298
299
		}

300
301
302
303
304
		startKeyshareSession(
			session,
			session.Handler,
			builders,
			session.irmaSession,
305
			session.client.Configuration,
306
			session.client.keyshareServers,
307
			session.client.state,
308
		)
309
	}
Sietse Ringers's avatar
Sietse Ringers committed
310
}
311

Sietse Ringers's avatar
Sietse Ringers committed
312
313
314
315
func (session *session) KeyshareDone(message interface{}) {
	session.sendResponse(message)
}

Sietse Ringers's avatar
Sietse Ringers committed
316
func (session *session) KeyshareCancelled() {
317
	session.cancel()
Sietse Ringers's avatar
Sietse Ringers committed
318
319
}

Sietse Ringers's avatar
Sietse Ringers committed
320
func (session *session) KeyshareBlocked(duration int) {
321
	session.fail(&irma.SessionError{ErrorType: irma.ErrorKeyshareBlocked, Info: strconv.Itoa(duration)})
Sietse Ringers's avatar
Sietse Ringers committed
322
323
324
}

func (session *session) KeyshareError(err error) {
325
	session.fail(&irma.SessionError{ErrorType: irma.ErrorKeyshare, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
326
327
}

Sietse Ringers's avatar
Sietse Ringers committed
328
329
330
331
332
333
334
335
func (session *session) KeysharePin() {
	session.Handler.StatusUpdate(session.Action, irma.StatusConnected)
}

func (session *session) KeysharePinOK() {
	session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating)
}

Sietse Ringers's avatar
Sietse Ringers committed
336
337
type disclosureResponse string

Sietse Ringers's avatar
Sietse Ringers committed
338
func (session *session) sendResponse(message interface{}) {
Sietse Ringers's avatar
Sietse Ringers committed
339
	var log *LogEntry
Sietse Ringers's avatar
Sietse Ringers committed
340
	var err error
Sietse Ringers's avatar
Sietse Ringers committed
341

Sietse Ringers's avatar
Sietse Ringers committed
342
	switch session.Action {
343
	case irma.ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
344
		fallthrough
345
	case irma.ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
346
		var response disclosureResponse
Sietse Ringers's avatar
Sietse Ringers committed
347
		if err = session.transport.Post("proofs", &response, message); err != nil {
348
			session.fail(err.(*irma.SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
349
350
351
			return
		}
		if response != "VALID" {
352
			session.fail(&irma.SessionError{ErrorType: irma.ErrorRejected, Info: string(response)})
Sietse Ringers's avatar
Sietse Ringers committed
353
354
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
355
		log, _ = session.createLogEntry(message.(gabi.ProofList)) // TODO err
356
	case irma.ActionIssuing:
Sietse Ringers's avatar
Sietse Ringers committed
357
		response := []*gabi.IssueSignatureMessage{}
Sietse Ringers's avatar
Sietse Ringers committed
358
		if err = session.transport.Post("commitments", &response, message); err != nil {
359
			session.fail(err.(*irma.SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
360
361
			return
		}
362
363
		if err = session.client.ConstructCredentials(response, session.irmaSession.(*irma.IssuanceRequest)); err != nil {
			session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
364
365
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
366
		log, _ = session.createLogEntry(message) // TODO err
367
368
	}

369
	_ = session.client.addLogEntry(log) // TODO err
370
	if !session.downloaded.Empty() {
371
		session.client.handler.UpdateConfiguration(session.downloaded)
372
	}
373
	if session.Action == irma.ActionIssuing {
374
		session.client.handler.UpdateAttributes()
375
	}
376
	session.done = true
377
	session.Handler.Success(session.Action)
378
}
379
380

func (session *session) managerSession() {
381
382
383
384
385
386
387
388
389
390
391
	defer func() {
		handlePanic(func(err *irma.SessionError) {
			if session.Handler != nil {
				session.Handler.Failure(session.Action, err)
			}
		})
	}()

	// We have to download the scheme manager description.xml here before installing it,
	// because we need to show its contents (name, description, website) to the user
	// when asking installation permission.
392
	manager, err := session.client.Configuration.DownloadSchemeManager(session.ServerURL)
393
	if err != nil {
394
		session.Handler.Failure(session.Action, &irma.SessionError{ErrorType: irma.ErrorConfigurationDownload, Err: err})
395
396
		return
	}
397

398
	session.Handler.RequestSchemeManagerPermission(manager, func(proceed bool) {
399
		if !proceed {
400
			session.Handler.Cancelled(session.Action) // No need to DELETE session here
401
402
			return
		}
403
		if err := session.client.Configuration.AddSchemeManager(manager); err != nil {
404
			session.Handler.Failure(session.Action, &irma.SessionError{ErrorType: irma.ErrorConfigurationDownload, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
405
406
			return
		}
407
408

		// Update state and inform user of success
409
		if manager.Distributed() {
410
			session.client.UnenrolledSchemeManagers = session.client.unenrolledSchemeManagers()
411
		}
412
		session.client.handler.UpdateConfiguration(
413
414
415
416
			&irma.IrmaIdentifierSet{
				SchemeManagers:  map[irma.SchemeManagerIdentifier]struct{}{manager.Identifier(): {}},
				Issuers:         map[irma.IssuerIdentifier]struct{}{},
				CredentialTypes: map[irma.CredentialTypeIdentifier]struct{}{},
Sietse Ringers's avatar
Sietse Ringers committed
417
418
419
			},
		)
		session.Handler.Success(session.Action)
420
421
422
	})
	return
}
423
424
425

// Session lifetime functions

426
func handlePanic(callback func(*irma.SessionError)) {
427
428
429
430
431
432
433
434
435
436
437
438
439
	if e := recover(); e != nil {
		var info string
		switch x := e.(type) {
		case string:
			info = x
		case error:
			info = x.Error()
		case fmt.Stringer:
			info = x.String()
		default: // nop
		}
		fmt.Printf("Recovered from panic: '%v'\n%s\n", e, info)
		if callback != nil {
440
			callback(&irma.SessionError{ErrorType: irma.ErrorPanic, Info: info})
441
442
443
444
445
446
		}
	}
}

// Idempotently send DELETE to remote server, returning whether or not we did something
func (session *session) delete() bool {
447
	if !session.done {
448
		session.transport.Delete()
449
		session.done = true
450
451
452
453
454
		return true
	}
	return false
}

455
func (session *session) fail(err *irma.SessionError) {
456
457
458
	if session.delete() {
		err.Err = errors.Wrap(err.Err, 0)
		if session.downloaded != nil && !session.downloaded.Empty() {
459
			session.client.handler.UpdateConfiguration(session.downloaded)
460
461
462
463
464
465
466
467
		}
		session.Handler.Failure(session.Action, err)
	}
}

func (session *session) cancel() {
	if session.delete() {
		if session.downloaded != nil && !session.downloaded.Empty() {
468
			session.client.handler.UpdateConfiguration(session.downloaded)
469
470
471
472
473
474
475
476
		}
		session.Handler.Cancelled(session.Action)
	}
}

func (session *session) Dismiss() {
	session.cancel()
}
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492

type issuanceState struct {
	nonce2   *big.Int
	builders []*gabi.CredentialBuilder
}

func newIssuanceState() (*issuanceState, error) {
	nonce2, err := gabi.RandomBigInt(gabi.DefaultSystemParameters[4096].Lstatzk)
	if err != nil {
		return nil, err
	}
	return &issuanceState{
		nonce2:   nonce2,
		builders: []*gabi.CredentialBuilder{},
	}, nil
}