session.go 9.17 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
22
23
24

// 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
25
	Failure(action Action, err *SessionError)
Sietse Ringers's avatar
Sietse Ringers committed
26
	UnsatisfiableRequest(action Action, missing AttributeDisjunctionList)
27

Sietse Ringers's avatar
Sietse Ringers committed
28
29
30
	AskIssuancePermission(request IssuanceRequest, ServerName string, callback PermissionHandler)
	AskVerificationPermission(request DisclosureRequest, ServerName string, callback PermissionHandler)
	AskSignaturePermission(request SignatureRequest, ServerName string, callback PermissionHandler)
Sietse Ringers's avatar
Sietse Ringers committed
31

Sietse Ringers's avatar
Sietse Ringers committed
32
	AskPin(remainingAttempts int, callback func(proceed bool, pin string))
33
34
}

Sietse Ringers's avatar
Sietse Ringers committed
35
36
// A session is an IRMA session.
type session struct {
37
38
39
40
41
	Action    Action
	Version   Version
	ServerURL string
	Handler   Handler

Sietse Ringers's avatar
Sietse Ringers committed
42
	info        *SessionInfo
43
	credManager *CredentialManager
Sietse Ringers's avatar
Sietse Ringers committed
44
	jwt         RequestorJwt
45
	irmaSession IrmaSession
Sietse Ringers's avatar
Sietse Ringers committed
46
47
	transport   *HTTPTransport
	choice      *DisclosureChoice
48
49
}

50
51
52
// We implement the handler for the keyshare protocol
var _ keyshareSessionHandler = (*session)(nil)

53
54
// Supported protocol versions. Minor version numbers should be reverse sorted.
var supportedVersions = map[int][]int{
55
	2: {2, 1},
56
57
58
59
}

func calcVersion(qr *Qr) (string, error) {
	// Parse range supported by server
60
61
62
63
64
65
66
67
68
69
70
71
	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 {
72
73
74
75
76
		return "", err
	}

	// Iterate supportedVersions in reverse sorted order (i.e. biggest major number first)
	keys := make([]int, 0, len(supportedVersions))
77
	for k := range supportedVersions {
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
		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)
}

93
// NewSession creates and starts a new IRMA session.
94
func (cm *CredentialManager) NewSession(qr *Qr, handler Handler) {
Sietse Ringers's avatar
Sietse Ringers committed
95
	session := &session{
96
97
98
99
		Action:      Action(qr.Type),
		ServerURL:   qr.URL,
		Handler:     handler,
		transport:   NewHTTPTransport(qr.URL),
100
		credManager: cm,
101
	}
102
103
	version, err := calcVersion(qr)
	if err != nil {
Tomas's avatar
Tomas committed
104
		session.fail(&SessionError{ErrorType: ErrorProtocolVersionNotSupported, Err: err})
105
106
107
		return
	}
	session.Version = Version(version)
108
109
110
111
112
113
114
115
116

	// Check if the action is one of the supported types
	switch session.Action {
	case ActionDisclosing: // nop
	case ActionSigning: // nop
	case ActionIssuing: // nop
	case ActionUnknown:
		fallthrough
	default:
Tomas's avatar
Tomas committed
117
		session.fail(&SessionError{ErrorType: ErrorUnknownAction, Info: string(session.Action)})
Sietse Ringers's avatar
Sietse Ringers committed
118
		return
119
120
121
122
123
124
125
126
	}

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

	go session.start()

Sietse Ringers's avatar
Sietse Ringers committed
127
	return
128
129
}

Tomas's avatar
Tomas committed
130
func (session *session) fail(err *SessionError) {
131
132
133
134
135
	session.transport.Delete()
	err.Err = errors.Wrap(err.Err, 0)
	session.Handler.Failure(session.Action, err)
}

136
137
// 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
138
func (session *session) start() {
139
140
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)

Sietse Ringers's avatar
Sietse Ringers committed
141
	// Get the first IRMA protocol message and parse it
Sietse Ringers's avatar
Sietse Ringers committed
142
143
	session.info = &SessionInfo{}
	Err := session.transport.Get("jwt", session.info)
Sietse Ringers's avatar
Sietse Ringers committed
144
	if Err != nil {
Tomas's avatar
Tomas committed
145
		session.fail(Err.(*SessionError))
146
147
148
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
149
150
151
	var server string
	var err error
	session.jwt, server, err = parseRequestorJwt(session.Action, session.info.Jwt)
Sietse Ringers's avatar
Sietse Ringers committed
152
	if err != nil {
Tomas's avatar
Tomas committed
153
		session.fail(&SessionError{ErrorType: ErrorInvalidJWT, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
154
155
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
156
	session.irmaSession = session.jwt.IrmaSession()
Sietse Ringers's avatar
Sietse Ringers committed
157
158
	session.irmaSession.SetContext(session.info.Context)
	session.irmaSession.SetNonce(session.info.Nonce)
159
160
	if session.Action == ActionIssuing {
		// Store which public keys the server will use
Sietse Ringers's avatar
Sietse Ringers committed
161
		for _, credreq := range session.irmaSession.(*IssuanceRequest).Credentials {
Sietse Ringers's avatar
Sietse Ringers committed
162
			credreq.KeyCounter = session.info.Keys[credreq.Credential.IssuerIdentifier()]
163
164
165
		}
	}

166
167
168
169
170
171
172
173
	if err = session.credManager.ConfigurationStore.Download(session.irmaSession.Identifiers()); err != nil {
		session.Handler.Failure(
			session.Action,
			&SessionError{ErrorType: ErrorConfigurationStoreDownload, Err: err},
		)
		return
	}

174
	missing := session.credManager.CheckSatisfiability(session.irmaSession.DisjunctionList())
175
176
	if len(missing) > 0 {
		session.Handler.UnsatisfiableRequest(session.Action, missing)
177
		// TODO: session.transport.Delete() on dialog cancel
178
179
180
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
181
	// Ask for permission to execute the session
Sietse Ringers's avatar
Sietse Ringers committed
182
	callback := PermissionHandler(func(proceed bool, choice *DisclosureChoice) {
Sietse Ringers's avatar
Sietse Ringers committed
183
184
185
		session.choice = choice
		session.irmaSession.SetDisclosureChoice(choice)
		go session.do(proceed)
186
187
188
189
	})
	session.Handler.StatusUpdate(session.Action, StatusConnected)
	switch session.Action {
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
190
		session.Handler.AskVerificationPermission(*session.irmaSession.(*DisclosureRequest), server, callback)
191
	case ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
192
		session.Handler.AskSignaturePermission(*session.irmaSession.(*SignatureRequest), server, callback)
193
	case ActionIssuing:
Sietse Ringers's avatar
Sietse Ringers committed
194
		session.Handler.AskIssuancePermission(*session.irmaSession.(*IssuanceRequest), server, callback)
195
196
197
198
199
	default:
		panic("Invalid session type") // does not happen, session.Action has been checked earlier
	}
}

Sietse Ringers's avatar
Sietse Ringers committed
200
func (session *session) do(proceed bool) {
201
	if !proceed {
202
		session.transport.Delete()
203
204
205
206
		session.Handler.Cancelled(session.Action)
		return
	}
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)
207

208
	if !session.irmaSession.Identifiers().Distributed(session.credManager.ConfigurationStore) {
Sietse Ringers's avatar
Sietse Ringers committed
209
210
211
212
		var message interface{}
		var err error
		switch session.Action {
		case ActionSigning:
213
			message, err = session.credManager.Proofs(session.choice, session.irmaSession, true)
Sietse Ringers's avatar
Sietse Ringers committed
214
		case ActionDisclosing:
215
			message, err = session.credManager.Proofs(session.choice, session.irmaSession, false)
Sietse Ringers's avatar
Sietse Ringers committed
216
		case ActionIssuing:
217
			message, err = session.credManager.IssueCommitments(session.irmaSession.(*IssuanceRequest))
Sietse Ringers's avatar
Sietse Ringers committed
218
219
		}
		if err != nil {
Tomas's avatar
Tomas committed
220
			session.fail(&SessionError{ErrorType: ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
221
222
223
224
			return
		}
		session.sendResponse(message)
	} else {
225
		var builders gabi.ProofBuilderList
Sietse Ringers's avatar
Sietse Ringers committed
226
227
228
229
230
		var err error
		switch session.Action {
		case ActionSigning:
			fallthrough
		case ActionDisclosing:
231
			builders, err = session.credManager.ProofBuilders(session.choice)
Sietse Ringers's avatar
Sietse Ringers committed
232
		case ActionIssuing:
233
			builders, err = session.credManager.IssuanceProofBuilders(session.irmaSession.(*IssuanceRequest))
Sietse Ringers's avatar
Sietse Ringers committed
234
235
		}
		if err != nil {
Tomas's avatar
Tomas committed
236
			session.fail(&SessionError{ErrorType: ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
237
238
		}

239
240
241
242
243
244
245
246
		startKeyshareSession(
			session,
			session.Handler,
			builders,
			session.irmaSession,
			session.credManager.ConfigurationStore,
			session.credManager.keyshareServers,
		)
247
	}
Sietse Ringers's avatar
Sietse Ringers committed
248
}
249

Sietse Ringers's avatar
Sietse Ringers committed
250
251
252
253
func (session *session) KeyshareDone(message interface{}) {
	session.sendResponse(message)
}

Sietse Ringers's avatar
Sietse Ringers committed
254
func (session *session) KeyshareCancelled() {
255
	session.transport.Delete()
Sietse Ringers's avatar
Sietse Ringers committed
256
257
258
	session.Handler.Cancelled(session.Action)
}

Sietse Ringers's avatar
Sietse Ringers committed
259
func (session *session) KeyshareBlocked(duration int) {
Tomas's avatar
Tomas committed
260
	session.fail(&SessionError{ErrorType: ErrorKeyshareBlocked, Info: strconv.Itoa(duration)})
Sietse Ringers's avatar
Sietse Ringers committed
261
262
263
}

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

Sietse Ringers's avatar
Sietse Ringers committed
267
268
type disclosureResponse string

Sietse Ringers's avatar
Sietse Ringers committed
269
func (session *session) sendResponse(message interface{}) {
Sietse Ringers's avatar
Sietse Ringers committed
270
	var log *LogEntry
Sietse Ringers's avatar
Sietse Ringers committed
271
	var err error
Sietse Ringers's avatar
Sietse Ringers committed
272

Sietse Ringers's avatar
Sietse Ringers committed
273
274
275
276
	switch session.Action {
	case ActionSigning:
		fallthrough
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
277
		var response disclosureResponse
Sietse Ringers's avatar
Sietse Ringers committed
278
		if err = session.transport.Post("proofs", &response, message); err != nil {
Tomas's avatar
Tomas committed
279
			session.fail(err.(*SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
280
281
282
			return
		}
		if response != "VALID" {
Tomas's avatar
Tomas committed
283
			session.fail(&SessionError{ErrorType: ErrorRejected, Info: string(response)})
Sietse Ringers's avatar
Sietse Ringers committed
284
285
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
286
		log, _ = session.createLogEntry(message.(gabi.ProofList)) // TODO err
Sietse Ringers's avatar
Sietse Ringers committed
287
288
	case ActionIssuing:
		response := []*gabi.IssueSignatureMessage{}
Sietse Ringers's avatar
Sietse Ringers committed
289
		if err = session.transport.Post("commitments", &response, message); err != nil {
Tomas's avatar
Tomas committed
290
			session.fail(err.(*SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
291
292
			return
		}
293
		if err = session.credManager.ConstructCredentials(response, session.irmaSession.(*IssuanceRequest)); err != nil {
Tomas's avatar
Tomas committed
294
			session.fail(&SessionError{ErrorType: ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
295
296
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
297
		log, _ = session.createLogEntry(message) // TODO err
298
299
	}

300
	_ = session.credManager.addLogEntry(log) // TODO err
301
	session.Handler.Success(session.Action)
302
}