session.go 13.1 KB
Newer Older
Sietse Ringers's avatar
Sietse Ringers committed
1
package irmago
2
3

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

9
	"github.com/go-errors/errors"
10
	"github.com/mhe/gabi"
11
12
)

Sietse Ringers's avatar
Sietse Ringers committed
13
14
15
// 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
16
17
// PermissionHandler is a callback for providing permission for an IRMA session
// and specifying the attributes to be disclosed.
Sietse Ringers's avatar
Sietse Ringers committed
18
type PermissionHandler func(proceed bool, choice *DisclosureChoice)
19

20
21
type PinHandler func(proceed bool, pin string)

22
23
24
25
26
// A Handler contains callbacks for communication to the user.
type Handler interface {
	StatusUpdate(action Action, status Status)
	Success(action Action)
	Cancelled(action Action)
Tomas's avatar
Tomas committed
27
	Failure(action Action, err *SessionError)
Sietse Ringers's avatar
Sietse Ringers committed
28
	UnsatisfiableRequest(action Action, missing AttributeDisjunctionList)
29
	MissingKeyshareEnrollment(manager SchemeManagerIdentifier)
30

31
32
33
	RequestIssuancePermission(request IssuanceRequest, ServerName string, callback PermissionHandler)
	RequestVerificationPermission(request DisclosureRequest, ServerName string, callback PermissionHandler)
	RequestSignaturePermission(request SignatureRequest, ServerName string, callback PermissionHandler)
34
	RequestSchemeManagerPermission(manager *SchemeManager, callback func(proceed bool))
Sietse Ringers's avatar
Sietse Ringers committed
35

36
	RequestPin(remainingAttempts int, callback PinHandler)
37
38
}

39
40
41
42
type SessionDismisser interface {
	Dismiss()
}

Sietse Ringers's avatar
Sietse Ringers committed
43
44
// A session is an IRMA session.
type session struct {
45
46
47
48
49
	Action    Action
	Version   Version
	ServerURL string
	Handler   Handler

Sietse Ringers's avatar
Sietse Ringers committed
50
	info        *SessionInfo
51
	client      *Client
Sietse Ringers's avatar
Sietse Ringers committed
52
	jwt         RequestorJwt
53
	irmaSession IrmaSession
Sietse Ringers's avatar
Sietse Ringers committed
54
55
	transport   *HTTPTransport
	choice      *DisclosureChoice
56
	downloaded  *IrmaIdentifierSet
57
	done        bool
58
59
}

60
61
62
// We implement the handler for the keyshare protocol
var _ keyshareSessionHandler = (*session)(nil)

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

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

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

103
// NewSession creates and starts a new IRMA session.
104
func (client *Client) NewSession(qr *Qr, handler Handler) SessionDismisser {
Sietse Ringers's avatar
Sietse Ringers committed
105
	session := &session{
106
107
108
109
110
		Action:    Action(qr.Type),
		ServerURL: qr.URL,
		Handler:   handler,
		transport: NewHTTPTransport(qr.URL),
		client:    client,
111
	}
112
113
	version, err := calcVersion(qr)
	if err != nil {
Tomas's avatar
Tomas committed
114
		session.fail(&SessionError{ErrorType: ErrorProtocolVersionNotSupported, Err: err})
115
		return nil
116
117
	}
	session.Version = Version(version)
118
119
120
121
122
123

	// Check if the action is one of the supported types
	switch session.Action {
	case ActionDisclosing: // nop
	case ActionSigning: // nop
	case ActionIssuing: // nop
124
	//case ActionSchemeManager: // nop
125
126
127
	case ActionUnknown:
		fallthrough
	default:
Tomas's avatar
Tomas committed
128
		session.fail(&SessionError{ErrorType: ErrorUnknownAction, Info: string(session.Action)})
129
		return nil
130
131
132
133
134
135
136
137
	}

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

	go session.start()

138
	return session
139
140
}

141
142
// 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
143
func (session *session) start() {
144
145
146
147
148
149
150
151
	defer func() {
		handlePanic(func(err *SessionError) {
			if session.Handler != nil {
				session.Handler.Failure(session.Action, err)
			}
		})
	}()

152
153
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)

154
155
156
157
158
	if session.Action == ActionSchemeManager {
		session.managerSession()
		return
	}

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

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

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

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

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

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

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

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

260
	if !proceed {
261
		session.cancel()
262
263
264
		return
	}
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)
265

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

297
298
299
300
301
		startKeyshareSession(
			session,
			session.Handler,
			builders,
			session.irmaSession,
302
303
			session.client.ConfigurationStore,
			session.client.keyshareServers,
304
		)
305
	}
Sietse Ringers's avatar
Sietse Ringers committed
306
}
307

Sietse Ringers's avatar
Sietse Ringers committed
308
309
310
311
func (session *session) KeyshareDone(message interface{}) {
	session.sendResponse(message)
}

Sietse Ringers's avatar
Sietse Ringers committed
312
func (session *session) KeyshareCancelled() {
313
	session.cancel()
Sietse Ringers's avatar
Sietse Ringers committed
314
315
}

Sietse Ringers's avatar
Sietse Ringers committed
316
func (session *session) KeyshareBlocked(duration int) {
Tomas's avatar
Tomas committed
317
	session.fail(&SessionError{ErrorType: ErrorKeyshareBlocked, Info: strconv.Itoa(duration)})
Sietse Ringers's avatar
Sietse Ringers committed
318
319
320
}

func (session *session) KeyshareError(err error) {
Tomas's avatar
Tomas committed
321
	session.fail(&SessionError{ErrorType: ErrorKeyshare, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
322
323
}

Sietse Ringers's avatar
Sietse Ringers committed
324
325
type disclosureResponse string

Sietse Ringers's avatar
Sietse Ringers committed
326
func (session *session) sendResponse(message interface{}) {
Sietse Ringers's avatar
Sietse Ringers committed
327
	var log *LogEntry
Sietse Ringers's avatar
Sietse Ringers committed
328
	var err error
Sietse Ringers's avatar
Sietse Ringers committed
329

Sietse Ringers's avatar
Sietse Ringers committed
330
331
332
333
	switch session.Action {
	case ActionSigning:
		fallthrough
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
334
		var response disclosureResponse
Sietse Ringers's avatar
Sietse Ringers committed
335
		if err = session.transport.Post("proofs", &response, message); err != nil {
Tomas's avatar
Tomas committed
336
			session.fail(err.(*SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
337
338
339
			return
		}
		if response != "VALID" {
Tomas's avatar
Tomas committed
340
			session.fail(&SessionError{ErrorType: ErrorRejected, Info: string(response)})
Sietse Ringers's avatar
Sietse Ringers committed
341
342
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
343
		log, _ = session.createLogEntry(message.(gabi.ProofList)) // TODO err
Sietse Ringers's avatar
Sietse Ringers committed
344
345
	case ActionIssuing:
		response := []*gabi.IssueSignatureMessage{}
Sietse Ringers's avatar
Sietse Ringers committed
346
		if err = session.transport.Post("commitments", &response, message); err != nil {
Tomas's avatar
Tomas committed
347
			session.fail(err.(*SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
348
349
			return
		}
350
		if err = session.client.ConstructCredentials(response, session.irmaSession.(*IssuanceRequest)); err != nil {
Tomas's avatar
Tomas committed
351
			session.fail(&SessionError{ErrorType: ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
352
353
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
354
		log, _ = session.createLogEntry(message) // TODO err
355
356
	}

357
	_ = session.client.addLogEntry(log) // TODO err
358
	if !session.downloaded.Empty() {
359
		session.client.handler.UpdateConfigurationStore(session.downloaded)
360
361
	}
	if session.Action == ActionIssuing {
362
		session.client.handler.UpdateAttributes()
363
	}
364
	session.done = true
365
	session.Handler.Success(session.Action)
366
}
367
368

func (session *session) managerSession() {
369
	manager, err := session.client.ConfigurationStore.DownloadSchemeManager(session.ServerURL)
370
371
372
373
	if err != nil {
		session.Handler.Failure(session.Action, &SessionError{Err: err}) // TODO
		return
	}
374
	session.Handler.RequestSchemeManagerPermission(manager, func(proceed bool) {
375
		if !proceed {
376
			session.Handler.Cancelled(session.Action) // No need to DELETE session here
377
378
			return
		}
379
		if err := session.client.ConfigurationStore.AddSchemeManager(manager); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
380
381
382
			session.Handler.Failure(session.Action, &SessionError{})
			return
		}
383
		if manager.Distributed() {
384
			session.client.UnenrolledSchemeManagers = session.client.unenrolledSchemeManagers()
385
		}
386
		session.client.handler.UpdateConfigurationStore(
Sietse Ringers's avatar
Sietse Ringers committed
387
388
389
390
391
392
393
			&IrmaIdentifierSet{
				SchemeManagers:  map[SchemeManagerIdentifier]struct{}{manager.Identifier(): {}},
				Issuers:         map[IssuerIdentifier]struct{}{},
				CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
			},
		)
		session.Handler.Success(session.Action)
394
395
396
	})
	return
}
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420

// Session lifetime functions

func handlePanic(callback func(*SessionError)) {
	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 {
			callback(&SessionError{ErrorType: ErrorPanic, Info: info})
		}
	}
}

// Idempotently send DELETE to remote server, returning whether or not we did something
func (session *session) delete() bool {
421
	if !session.done {
422
		session.transport.Delete()
423
		session.done = true
424
425
426
427
428
429
430
431
432
		return true
	}
	return false
}

func (session *session) fail(err *SessionError) {
	if session.delete() {
		err.Err = errors.Wrap(err.Err, 0)
		if session.downloaded != nil && !session.downloaded.Empty() {
433
			session.client.handler.UpdateConfigurationStore(session.downloaded)
434
435
436
437
438
439
440
441
		}
		session.Handler.Failure(session.Action, err)
	}
}

func (session *session) cancel() {
	if session.delete() {
		if session.downloaded != nil && !session.downloaded.Empty() {
442
			session.client.handler.UpdateConfigurationStore(session.downloaded)
443
444
445
446
447
448
449
450
		}
		session.Handler.Cancelled(session.Action)
	}
}

func (session *session) Dismiss() {
	session.cancel()
}