session.go 8.26 KB
Newer Older
1
2
3
4
package protocol

import (
	"math/big"
5
	"strconv"
6
7
	"strings"

8
9
10
11
	"sort"

	"fmt"

12
13
14
15
	"encoding/json"

	"encoding/base64"

16
	"github.com/credentials/irmago"
17
	"github.com/mhe/gabi"
18
19
)

20
type PermissionHandler func(proceed bool, choice *irmago.DisclosureChoice)
21
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)
27
	Failure(action Action, err *Error)
28
29
	UnsatisfiableRequest(action Action, missing irmago.AttributeDisjunctionList)

30
31
32
	AskIssuancePermission(request irmago.IssuanceRequest, ServerName string, choice PermissionHandler)
	AskVerificationPermission(request irmago.DisclosureRequest, ServerName string, choice PermissionHandler)
	AskSignaturePermission(request irmago.SignatureRequest, ServerName string, choice PermissionHandler)
33
34
35
36
37
38
39
40
41
42
}

// A Session is an IRMA session.
type Session struct {
	Action    Action
	Version   Version
	ServerURL string
	Handler   Handler

	request   irmago.DisjunctionListContainer
43
44
45
	spRequest *ServiceProviderJwt
	ipRequest *IdentityProviderJwt
	ssRequest *SignatureServerJwt
46

47
48
49
50
51
	transport *HTTPTransport
	nonce     *big.Int
	context   *big.Int
}

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// Supported protocol versions. Minor version numbers should be reverse sorted.
var supportedVersions = map[int][]int{
	2: []int{2, 1},
}

func calcVersion(qr *Qr) (string, error) {
	// Parse range supported by server
	minmajor, err := strconv.Atoi(string(qr.ProtocolVersion[0]))
	minminor, err := strconv.Atoi(string(qr.ProtocolVersion[2]))
	maxmajor, err := strconv.Atoi(string(qr.ProtocolMaxVersion[0]))
	maxminor, err := strconv.Atoi(string(qr.ProtocolMaxVersion[2]))
	if err != nil {
		return "", err
	}

	// Iterate supportedVersions in reverse sorted order (i.e. biggest major number first)
	keys := make([]int, 0, len(supportedVersions))
	for k, _ := range supportedVersions {
		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.
86
func NewSession(qr *Qr, handler Handler) *Session {
87
88
	version, err := calcVersion(qr)
	if err != nil {
89
		handler.Failure(ActionUnknown, &Error{ErrorCode: ErrorProtocolVersionNotSupported, error: err})
90
91
92
93
		return nil
	}

	session := &Session{
94
		Version:   Version(version),
95
96
97
98
99
100
101
102
103
104
105
106
107
108
		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:
109
		handler.Failure(ActionUnknown, &Error{ErrorCode: ErrorUnknownAction, error: nil, info: string(session.Action)})
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
		return nil
	}

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

	go session.start()

	return session
}

// start retrieves the first message in the IRMA protocol, checks if we can perform
// the request, and informs the user of the outcome.
func (session *Session) start() {
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)

	// Get the first IRMA protocol message
	info := &SessionInfo{}
	err := session.transport.Get("jwt", info)
	if err != nil {
131
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorTransport, ApiError: err.(*TransportError).ApiErr})
132
133
134
135
136
137
138
		return
	}

	session.nonce = info.Nonce
	session.context = info.Context
	jwtparts := strings.Split(info.Jwt, ".")
	if jwtparts == nil || len(jwtparts) < 2 {
139
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT})
140
141
		return
	}
142
143
144
145
146
147
148
149

	headerbytes, err := base64.RawStdEncoding.DecodeString(jwtparts[0])
	bodybytes, err := base64.RawStdEncoding.DecodeString(jwtparts[1])
	if err != nil {
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT})
		return
	}

150
151
152
	var header struct {
		Server string `json:"iss"`
	}
Sietse Ringers's avatar
Sietse Ringers committed
153
154
155
156
157
	err = json.Unmarshal([]byte(headerbytes), &header)
	if err != nil {
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT})
		return
	}
158
159
160

	switch session.Action {
	case ActionDisclosing:
161
		session.spRequest = &ServiceProviderJwt{}
Sietse Ringers's avatar
Sietse Ringers committed
162
		err = json.Unmarshal([]byte(bodybytes), session.spRequest)
163
164
		session.spRequest.Request.Request.Context = session.context
		session.spRequest.Request.Request.Nonce = session.nonce
165
		session.request = session.spRequest
166
	case ActionSigning:
167
		session.ssRequest = &SignatureServerJwt{}
Sietse Ringers's avatar
Sietse Ringers committed
168
		err = json.Unmarshal([]byte(bodybytes), session.ssRequest)
169
170
		session.ssRequest.Request.Request.Context = session.context
		session.ssRequest.Request.Request.Nonce = session.nonce
171
		session.request = session.ssRequest
172
	case ActionIssuing:
173
		session.ipRequest = &IdentityProviderJwt{}
Sietse Ringers's avatar
Sietse Ringers committed
174
		err = json.Unmarshal([]byte(bodybytes), session.ipRequest)
175
176
		session.ipRequest.Request.Request.Context = session.context
		session.ipRequest.Request.Request.Nonce = session.nonce
177
		session.request = session.ipRequest
178
179
180
	default:
		panic("Invalid session type") // does not happen, session.Action has been checked earlier
	}
Sietse Ringers's avatar
Sietse Ringers committed
181
182
183
184
	if err != nil {
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT})
		return
	}
185

186
187
	if session.Action == ActionIssuing {
		// Store which public keys the server will use
188
		for _, credreq := range session.ipRequest.Request.Request.Credentials {
189
190
191
192
193
194
195
196
197
198
			credreq.KeyCounter = info.Keys[credreq.Credential.IssuerIdentifier()]
		}
	}

	missing := irmago.Manager.CheckSatisfiability(session.request)
	if len(missing) > 0 {
		session.Handler.UnsatisfiableRequest(session.Action, missing)
		return
	}

199
	callback := PermissionHandler(func(proceed bool, choice *irmago.DisclosureChoice) {
200
201
202
203
204
205
		go session.do(proceed, choice)
	})

	session.Handler.StatusUpdate(session.Action, StatusConnected)
	switch session.Action {
	case ActionDisclosing:
206
		session.Handler.AskVerificationPermission(session.spRequest.Request.Request, header.Server, callback)
207
	case ActionSigning:
208
		session.Handler.AskSignaturePermission(session.ssRequest.Request.Request, header.Server, callback)
209
	case ActionIssuing:
210
		session.Handler.AskIssuancePermission(session.ipRequest.Request.Request, header.Server, callback)
211
212
213
214
215
	default:
		panic("Invalid session type") // does not happen, session.Action has been checked earlier
	}
}

216
217
218
219
220
221
func (session *Session) do(proceed bool, choice *irmago.DisclosureChoice) {
	if !proceed {
		session.Handler.Cancelled(session.Action)
		return
	}
	session.Handler.StatusUpdate(session.Action, StatusCommunicating)
222

Sietse Ringers's avatar
Sietse Ringers committed
223
	var message interface{}
224
225
226
	var err error
	switch session.Action {
	case ActionSigning:
Sietse Ringers's avatar
Sietse Ringers committed
227
		message, err = irmago.Manager.Proofs(choice, &session.ssRequest.Request.Request, true)
228
	case ActionDisclosing:
Sietse Ringers's avatar
Sietse Ringers committed
229
		message, err = irmago.Manager.Proofs(choice, &session.spRequest.Request.Request, false)
230
	case ActionIssuing:
Sietse Ringers's avatar
Sietse Ringers committed
231
		message, err = irmago.Manager.IssueCommitments(choice, &session.ipRequest.Request.Request)
232
233
	}
	if err != nil {
234
		session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorCrypto, error: err})
235
236
237
		return
	}

Sietse Ringers's avatar
Sietse Ringers committed
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
	switch session.Action {
	case ActionSigning:
		fallthrough
	case ActionDisclosing:
		response := ""
		err = session.transport.Post("proofs", &response, message)
		if err != nil {
			session.Handler.Failure(session.Action,
				&Error{ErrorCode: ErrorTransport, ApiError: err.(*TransportError).ApiErr, info: err.Error()})
			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,
				&Error{ErrorCode: ErrorTransport, ApiError: err.(*TransportError).ApiErr, info: err.Error()})
			return
		}

		err = irmago.Manager.ConstructCredentials(response, &session.ipRequest.Request.Request)
		if err != nil {
			session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorCrypto, error: err})
			return
		}
267
268
269
	}

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