session.go 8.2 KB
Newer Older
1
2
3
package protocol

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

9
	"encoding/base64"
Sietse Ringers's avatar
Sietse Ringers committed
10
	"encoding/json"
11

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

16
type PermissionHandler func(proceed bool, choice *irmago.DisclosureChoice)
17
18
19
20
21
22

// A Handler contains callbacks for communication to the user.
type Handler interface {
	StatusUpdate(action Action, status Status)
	Success(action Action)
	Cancelled(action Action)
23
	Failure(action Action, err *Error)
24
25
	UnsatisfiableRequest(action Action, missing irmago.AttributeDisjunctionList)

Sietse Ringers's avatar
Sietse Ringers committed
26
27
28
	AskIssuancePermission(request irmago.IssuanceRequest, ServerName string, callback PermissionHandler)
	AskVerificationPermission(request irmago.DisclosureRequest, ServerName string, callback PermissionHandler)
	AskSignaturePermission(request irmago.SignatureRequest, ServerName string, callback PermissionHandler)
29
30
}

Sietse Ringers's avatar
Sietse Ringers committed
31
32
// A session is an IRMA session.
type session struct {
33
34
35
36
37
	Action    Action
	Version   Version
	ServerURL string
	Handler   Handler

Sietse Ringers's avatar
Sietse Ringers committed
38
39
40
	jwt         RequestorJwt
	irmaSession irmago.Session
	transport   *HTTPTransport
41
42
}

43
44
// Supported protocol versions. Minor version numbers should be reverse sorted.
var supportedVersions = map[int][]int{
Sietse Ringers's avatar
Sietse Ringers committed
45
	2: {2, 1},
46
47
48
49
}

func calcVersion(qr *Qr) (string, error) {
	// Parse range supported by server
50
51
52
53
54
55
56
57
58
59
60
61
	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 {
62
63
64
65
66
		return "", err
	}

	// Iterate supportedVersions in reverse sorted order (i.e. biggest major number first)
	keys := make([]int, 0, len(supportedVersions))
67
	for k := range supportedVersions {
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
		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)
}

83
// NewSession creates and starts a new IRMA session.
Sietse Ringers's avatar
Sietse Ringers committed
84
func NewSession(qr *Qr, handler Handler) {
85
86
	version, err := calcVersion(qr)
	if err != nil {
87
		handler.Failure(ActionUnknown, &Error{ErrorCode: ErrorProtocolVersionNotSupported, error: err})
Sietse Ringers's avatar
Sietse Ringers committed
88
		return
89
90
	}

Sietse Ringers's avatar
Sietse Ringers committed
91
	session := &session{
92
		Version:   Version(version),
93
94
95
96
97
98
99
100
101
102
103
104
105
106
		Action:    Action(qr.Type),
		ServerURL: qr.URL,
		Handler:   handler,
		transport: NewHTTPTransport(qr.URL),
	}

	// 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:
107
		handler.Failure(ActionUnknown, &Error{ErrorCode: ErrorUnknownAction, error: nil, info: string(session.Action)})
Sietse Ringers's avatar
Sietse Ringers committed
108
		return
109
110
111
112
113
114
115
116
	}

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

	go session.start()

Sietse Ringers's avatar
Sietse Ringers committed
117
	return
118
119
120
121
}

// 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
122
func (session *session) start() {
123
124
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)

Sietse Ringers's avatar
Sietse Ringers committed
125
	// Get the first IRMA protocol message and parse it
126
127
128
	info := &SessionInfo{}
	err := session.transport.Get("jwt", info)
	if err != nil {
129
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorTransport, ApiError: err.(*TransportError).ApiErr})
130
131
132
133
		return
	}
	jwtparts := strings.Split(info.Jwt, ".")
	if jwtparts == nil || len(jwtparts) < 2 {
134
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT})
135
136
		return
	}
137
	headerbytes, err := base64.RawStdEncoding.DecodeString(jwtparts[0])
138
139
140
141
	if err != nil {
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT, error: err})
		return
	}
142
143
144
	var header struct {
		Server string `json:"iss"`
	}
Sietse Ringers's avatar
Sietse Ringers committed
145
146
	err = json.Unmarshal([]byte(headerbytes), &header)
	if err != nil {
147
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT, error: err})
Sietse Ringers's avatar
Sietse Ringers committed
148
149
		return
	}
150

Sietse Ringers's avatar
Sietse Ringers committed
151
152
153
154
155
156
	// Deserialize JWT, and set session state
	bodybytes, err := base64.RawStdEncoding.DecodeString(jwtparts[1])
	if err != nil {
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT, error: err})
		return
	}
157
158
	switch session.Action {
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
159
160
161
		jwt := &ServiceProviderJwt{}
		err = json.Unmarshal([]byte(bodybytes), jwt)
		session.jwt = jwt
162
	case ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
163
164
165
		jwt := &SignatureRequestorJwt{}
		err = json.Unmarshal([]byte(bodybytes), jwt)
		session.jwt = jwt
166
	case ActionIssuing:
Sietse Ringers's avatar
Sietse Ringers committed
167
168
169
		jwt := &IdentityProviderJwt{}
		err = json.Unmarshal([]byte(bodybytes), jwt)
		session.jwt = jwt
170
171
172
	default:
		panic("Invalid session type") // does not happen, session.Action has been checked earlier
	}
Sietse Ringers's avatar
Sietse Ringers committed
173
	if err != nil {
174
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT, error: err})
Sietse Ringers's avatar
Sietse Ringers committed
175
176
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
177
178
179
	session.irmaSession = session.jwt.IrmaSession()
	session.irmaSession.SetContext(info.Context)
	session.irmaSession.SetNonce(info.Nonce)
180
181
	if session.Action == ActionIssuing {
		// Store which public keys the server will use
Sietse Ringers's avatar
Sietse Ringers committed
182
		for _, credreq := range session.irmaSession.(*irmago.IssuanceRequest).Credentials {
183
184
185
186
			credreq.KeyCounter = info.Keys[credreq.Credential.IssuerIdentifier()]
		}
	}

Sietse Ringers's avatar
Sietse Ringers committed
187
	missing := irmago.Manager.CheckSatisfiability(session.irmaSession.DisjunctionList())
188
189
190
191
192
	if len(missing) > 0 {
		session.Handler.UnsatisfiableRequest(session.Action, missing)
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
193
	// Ask for permission to execute the session
194
	callback := PermissionHandler(func(proceed bool, choice *irmago.DisclosureChoice) {
195
196
197
198
199
		go session.do(proceed, choice)
	})
	session.Handler.StatusUpdate(session.Action, StatusConnected)
	switch session.Action {
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
200
		session.Handler.AskVerificationPermission(*session.irmaSession.(*irmago.DisclosureRequest), header.Server, callback)
201
	case ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
202
		session.Handler.AskSignaturePermission(*session.irmaSession.(*irmago.SignatureRequest), header.Server, callback)
203
	case ActionIssuing:
Sietse Ringers's avatar
Sietse Ringers committed
204
		session.Handler.AskIssuancePermission(*session.irmaSession.(*irmago.IssuanceRequest), header.Server, callback)
205
206
207
208
209
	default:
		panic("Invalid session type") // does not happen, session.Action has been checked earlier
	}
}

Sietse Ringers's avatar
Sietse Ringers committed
210
func (session *session) do(proceed bool, choice *irmago.DisclosureChoice) {
211
212
213
214
215
	if !proceed {
		session.Handler.Cancelled(session.Action)
		return
	}
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)
216

Sietse Ringers's avatar
Sietse Ringers committed
217
	var message interface{}
218
219
220
	var err error
	switch session.Action {
	case ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
221
		message, err = irmago.Manager.Proofs(choice, session.irmaSession, true)
222
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
223
		message, err = irmago.Manager.Proofs(choice, session.irmaSession, false)
224
	case ActionIssuing:
Sietse Ringers's avatar
Sietse Ringers committed
225
		message, err = irmago.Manager.IssueCommitments(choice, session.irmaSession.(*irmago.IssuanceRequest))
226
227
	}
	if err != nil {
228
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorCrypto, error: err})
229
230
231
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
232
233
234
235
236
237
238
239
	switch session.Action {
	case ActionSigning:
		fallthrough
	case ActionDisclosing:
		response := ""
		err = session.transport.Post("proofs", &response, message)
		if err != nil {
			session.Handler.Failure(session.Action,
240
				&Error{ErrorCode: ErrorTransport, ApiError: err.(*TransportError).ApiErr, info: err.Error(), error: err})
Sietse Ringers's avatar
Sietse Ringers committed
241
242
243
244
245
246
247
248
249
250
251
			return
		}
		if response != "VALID" {
			session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorRejected, info: response})
			return
		}
	case ActionIssuing:
		response := []*gabi.IssueSignatureMessage{}
		err = session.transport.Post("commitments", &response, message)
		if err != nil {
			session.Handler.Failure(session.Action,
252
				&Error{ErrorCode: ErrorTransport, ApiError: err.(*TransportError).ApiErr, info: err.Error(), error: err})
Sietse Ringers's avatar
Sietse Ringers committed
253
254
255
			return
		}

Sietse Ringers's avatar
Sietse Ringers committed
256
		err = irmago.Manager.ConstructCredentials(response, session.irmaSession.(*irmago.IssuanceRequest))
Sietse Ringers's avatar
Sietse Ringers committed
257
258
259
260
		if err != nil {
			session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorCrypto, error: err})
			return
		}
261
262
263
	}

	session.Handler.Success(session.Action)
264
}