session.go 8.27 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
)

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.
18
type PermissionHandler func(proceed bool, choice *irmago.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)
25
	Failure(action Action, err *irmago.Error)
26
27
	UnsatisfiableRequest(action Action, missing irmago.AttributeDisjunctionList)

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

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

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

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

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

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

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

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

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

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

	go session.start()

Sietse Ringers's avatar
Sietse Ringers committed
119
	return
120
121
122
123
}

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

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

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

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

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

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

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

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

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

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