session.go 14.8 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
	"math/big"

11
	"github.com/privacybydesign/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

Sietse Ringers's avatar
Sietse Ringers committed
23
// PinHandler is used to provide the user's PIN code.
24
25
type PinHandler func(proceed bool, pin string)

26
27
// A Handler contains callbacks for communication to the user.
type Handler interface {
28
29
30
31
32
33
34
35
36
37
38
	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
39

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

Sietse Ringers's avatar
Sietse Ringers committed
43
// SessionDismisser can dismiss the current IRMA session.
44
45
46
47
type SessionDismisser interface {
	Dismiss()
}

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

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

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

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

73
func calcVersion(qr *irma.Qr) (string, error) {
74
	// Parse range supported by server
75
76
77
78
79
80
81
82
83
84
85
86
	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 {
87
88
89
90
91
		return "", err
	}

	// Iterate supportedVersions in reverse sorted order (i.e. biggest major number first)
	keys := make([]int, 0, len(supportedVersions))
92
	for k := range supportedVersions {
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
		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)
}

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

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

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

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

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

	go session.start()

148
	return session
149
150
}

151
152
// 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
153
func (session *session) start() {
154
	defer func() {
155
		if e := recover(); e != nil {
156
			if session.Handler != nil {
157
				session.Handler.Failure(session.Action, panicToError(e))
158
			}
159
		}
160
161
	}()

162
	session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating)
163

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

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

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

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

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

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

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

Sietse Ringers's avatar
Sietse Ringers committed
256
func (session *session) do(proceed bool) {
257
	defer func() {
258
		if e := recover(); e != nil {
259
			if session.Handler != nil {
260
				session.Handler.Failure(session.Action, panicToError(e))
261
			}
262
		}
263
264
	}()

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

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

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

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

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

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

func (session *session) KeyshareError(err error) {
327
328
329
330
331
332
333
334
	var serr *irma.SessionError
	var ok bool
	if serr, ok = err.(*irma.SessionError); !ok {
		serr = &irma.SessionError{ErrorType: irma.ErrorKeyshare, Err: err}
	} else {
		serr.ErrorType = irma.ErrorKeyshare
	}
	session.fail(serr)
Sietse Ringers's avatar
Sietse Ringers committed
335
336
}

Sietse Ringers's avatar
Sietse Ringers committed
337
338
339
340
341
342
343
344
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
345
346
type disclosureResponse string

Sietse Ringers's avatar
Sietse Ringers committed
347
func (session *session) sendResponse(message interface{}) {
Sietse Ringers's avatar
Sietse Ringers committed
348
	var log *LogEntry
Sietse Ringers's avatar
Sietse Ringers committed
349
	var err error
Sietse Ringers's avatar
Sietse Ringers committed
350

Sietse Ringers's avatar
Sietse Ringers committed
351
	switch session.Action {
352
	case irma.ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
353
		fallthrough
354
	case irma.ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
355
		var response disclosureResponse
Sietse Ringers's avatar
Sietse Ringers committed
356
		if err = session.transport.Post("proofs", &response, message); err != nil {
357
			session.fail(err.(*irma.SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
358
359
360
			return
		}
		if response != "VALID" {
361
			session.fail(&irma.SessionError{ErrorType: irma.ErrorRejected, Info: string(response)})
Sietse Ringers's avatar
Sietse Ringers committed
362
363
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
364
		log, _ = session.createLogEntry(message.(gabi.ProofList)) // TODO err
365
	case irma.ActionIssuing:
Sietse Ringers's avatar
Sietse Ringers committed
366
		response := []*gabi.IssueSignatureMessage{}
Sietse Ringers's avatar
Sietse Ringers committed
367
		if err = session.transport.Post("commitments", &response, message); err != nil {
368
			session.fail(err.(*irma.SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
369
370
			return
		}
371
372
		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
373
374
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
375
		log, _ = session.createLogEntry(message) // TODO err
376
377
	}

378
	_ = session.client.addLogEntry(log) // TODO err
379
	if !session.downloaded.Empty() {
380
		session.client.handler.UpdateConfiguration(session.downloaded)
381
	}
382
	if session.Action == irma.ActionIssuing {
383
		session.client.handler.UpdateAttributes()
384
	}
385
	session.done = true
386
	session.Handler.Success(session.Action)
387
}
388
389

func (session *session) managerSession() {
390
	defer func() {
391
		if e := recover(); e != nil {
392
			if session.Handler != nil {
393
				session.Handler.Failure(session.Action, panicToError(e))
394
			}
395
		}
396
397
398
399
400
	}()

	// 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.
401
	manager, err := session.client.Configuration.DownloadSchemeManager(session.ServerURL)
402
	if err != nil {
403
		session.Handler.Failure(session.Action, &irma.SessionError{ErrorType: irma.ErrorConfigurationDownload, Err: err})
404
405
		return
	}
406

407
	session.Handler.RequestSchemeManagerPermission(manager, func(proceed bool) {
408
		if !proceed {
409
			session.Handler.Cancelled(session.Action) // No need to DELETE session here
410
411
			return
		}
412
		if err := session.client.Configuration.AddSchemeManager(manager); err != nil {
413
			session.Handler.Failure(session.Action, &irma.SessionError{ErrorType: irma.ErrorConfigurationDownload, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
414
415
			return
		}
416
417

		// Update state and inform user of success
418
		if manager.Distributed() {
419
			session.client.UnenrolledSchemeManagers = session.client.unenrolledSchemeManagers()
420
		}
421
		session.client.handler.UpdateConfiguration(
422
423
424
425
			&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
426
427
428
			},
		)
		session.Handler.Success(session.Action)
429
430
431
	})
	return
}
432
433
434

// Session lifetime functions

435
436
437
438
439
440
441
442
443
444
func panicToError(e interface{}) *irma.SessionError {
	var info string
	switch x := e.(type) {
	case string:
		info = x
	case error:
		info = x.Error()
	case fmt.Stringer:
		info = x.String()
	default: // nop
445
	}
446
	return &irma.SessionError{ErrorType: irma.ErrorPanic, Info: info}
447
448
449
450
}

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

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

func (session *session) cancel() {
	if session.delete() {
		if session.downloaded != nil && !session.downloaded.Empty() {
472
			session.client.handler.UpdateConfiguration(session.downloaded)
473
474
475
476
477
478
479
480
		}
		session.Handler.Cancelled(session.Action)
	}
}

func (session *session) Dismiss() {
	session.cancel()
}
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496

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
}