session.go 8.75 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
// Supported protocol versions. Minor version numbers should be reverse sorted.
var supportedVersions = map[int][]int{
52
	2: {2, 1},
53
54
55
56
}

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

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

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

	// 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
114
		session.fail(&SessionError{ErrorType: ErrorUnknownAction, Info: string(session.Action)})
Sietse Ringers's avatar
Sietse Ringers committed
115
		return
116
117
118
119
120
121
122
123
	}

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

	go session.start()

Sietse Ringers's avatar
Sietse Ringers committed
124
	return
125
126
}

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

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

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

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

163
	missing := session.credManager.CheckSatisfiability(session.irmaSession.DisjunctionList())
164
165
	if len(missing) > 0 {
		session.Handler.UnsatisfiableRequest(session.Action, missing)
166
		// TODO: session.transport.Delete() on dialog cancel
167
168
169
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
170
	// Ask for permission to execute the session
Sietse Ringers's avatar
Sietse Ringers committed
171
	callback := PermissionHandler(func(proceed bool, choice *DisclosureChoice) {
Sietse Ringers's avatar
Sietse Ringers committed
172
173
174
		session.choice = choice
		session.irmaSession.SetDisclosureChoice(choice)
		go session.do(proceed)
175
176
177
178
	})
	session.Handler.StatusUpdate(session.Action, StatusConnected)
	switch session.Action {
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
179
		session.Handler.AskVerificationPermission(*session.irmaSession.(*DisclosureRequest), server, callback)
180
	case ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
181
		session.Handler.AskSignaturePermission(*session.irmaSession.(*SignatureRequest), server, callback)
182
	case ActionIssuing:
Sietse Ringers's avatar
Sietse Ringers committed
183
		session.Handler.AskIssuancePermission(*session.irmaSession.(*IssuanceRequest), server, callback)
184
185
186
187
188
	default:
		panic("Invalid session type") // does not happen, session.Action has been checked earlier
	}
}

Sietse Ringers's avatar
Sietse Ringers committed
189
func (session *session) do(proceed bool) {
190
	if !proceed {
191
		session.transport.Delete()
192
193
194
195
		session.Handler.Cancelled(session.Action)
		return
	}
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)
196

197
	if !session.irmaSession.Distributed(session.credManager.ConfigurationStore) {
Sietse Ringers's avatar
Sietse Ringers committed
198
199
200
201
		var message interface{}
		var err error
		switch session.Action {
		case ActionSigning:
202
			message, err = session.credManager.Proofs(session.choice, session.irmaSession, true)
Sietse Ringers's avatar
Sietse Ringers committed
203
		case ActionDisclosing:
204
			message, err = session.credManager.Proofs(session.choice, session.irmaSession, false)
Sietse Ringers's avatar
Sietse Ringers committed
205
		case ActionIssuing:
206
			message, err = session.credManager.IssueCommitments(session.irmaSession.(*IssuanceRequest))
Sietse Ringers's avatar
Sietse Ringers committed
207
208
		}
		if err != nil {
Tomas's avatar
Tomas committed
209
			session.fail(&SessionError{ErrorType: ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
210
211
212
213
			return
		}
		session.sendResponse(message)
	} else {
214
		var builders gabi.ProofBuilderList
Sietse Ringers's avatar
Sietse Ringers committed
215
216
217
218
219
		var err error
		switch session.Action {
		case ActionSigning:
			fallthrough
		case ActionDisclosing:
220
			builders, err = session.credManager.ProofBuilders(session.choice)
Sietse Ringers's avatar
Sietse Ringers committed
221
		case ActionIssuing:
222
			builders, err = session.credManager.IssuanceProofBuilders(session.irmaSession.(*IssuanceRequest))
Sietse Ringers's avatar
Sietse Ringers committed
223
224
		}
		if err != nil {
Tomas's avatar
Tomas committed
225
			session.fail(&SessionError{ErrorType: ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
226
227
		}

228
		startKeyshareSession(session.credManager, session.irmaSession, builders, session, session.Handler)
229
	}
Sietse Ringers's avatar
Sietse Ringers committed
230
}
231

Sietse Ringers's avatar
Sietse Ringers committed
232
233
234
235
func (session *session) KeyshareDone(message interface{}) {
	session.sendResponse(message)
}

Sietse Ringers's avatar
Sietse Ringers committed
236
func (session *session) KeyshareCancelled() {
237
	session.transport.Delete()
Sietse Ringers's avatar
Sietse Ringers committed
238
239
240
	session.Handler.Cancelled(session.Action)
}

Sietse Ringers's avatar
Sietse Ringers committed
241
func (session *session) KeyshareBlocked(duration int) {
Tomas's avatar
Tomas committed
242
	session.fail(&SessionError{ErrorType: ErrorKeyshareBlocked, Info: strconv.Itoa(duration)})
Sietse Ringers's avatar
Sietse Ringers committed
243
244
245
}

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

Sietse Ringers's avatar
Sietse Ringers committed
249
250
type disclosureResponse string

Sietse Ringers's avatar
Sietse Ringers committed
251
func (session *session) sendResponse(message interface{}) {
Sietse Ringers's avatar
Sietse Ringers committed
252
	var log *LogEntry
Sietse Ringers's avatar
Sietse Ringers committed
253
	var err error
Sietse Ringers's avatar
Sietse Ringers committed
254

Sietse Ringers's avatar
Sietse Ringers committed
255
256
257
258
	switch session.Action {
	case ActionSigning:
		fallthrough
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
259
		var response disclosureResponse
Sietse Ringers's avatar
Sietse Ringers committed
260
		if err = session.transport.Post("proofs", &response, message); err != nil {
Tomas's avatar
Tomas committed
261
			session.fail(err.(*SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
262
263
264
			return
		}
		if response != "VALID" {
Tomas's avatar
Tomas committed
265
			session.fail(&SessionError{ErrorType: ErrorRejected, Info: string(response)})
Sietse Ringers's avatar
Sietse Ringers committed
266
267
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
268
		log, _ = session.createLogEntry(message.(gabi.ProofList)) // TODO err
Sietse Ringers's avatar
Sietse Ringers committed
269
270
	case ActionIssuing:
		response := []*gabi.IssueSignatureMessage{}
Sietse Ringers's avatar
Sietse Ringers committed
271
		if err = session.transport.Post("commitments", &response, message); err != nil {
Tomas's avatar
Tomas committed
272
			session.fail(err.(*SessionError))
Sietse Ringers's avatar
Sietse Ringers committed
273
274
			return
		}
275
		if err = session.credManager.ConstructCredentials(response, session.irmaSession.(*IssuanceRequest)); err != nil {
Tomas's avatar
Tomas committed
276
			session.fail(&SessionError{ErrorType: ErrorCrypto, Err: err})
Sietse Ringers's avatar
Sietse Ringers committed
277
278
			return
		}
Sietse Ringers's avatar
Sietse Ringers committed
279
		log, _ = session.createLogEntry(message) // TODO err
280
281
	}

282
	session.credManager.addLogEntry(log, true) // TODO err
283
	session.Handler.Success(session.Action)
284
}