session_test.go 13.6 KB
Newer Older
Sietse Ringers's avatar
Sietse Ringers committed
1
2
3
4
5
6
7
8
package irmaclient

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"testing"
9
	"time"
Sietse Ringers's avatar
Sietse Ringers committed
10
11

	"github.com/go-errors/errors"
12
	"github.com/privacybydesign/irmago"
13
	"github.com/privacybydesign/irmago/internal/test"
Sietse Ringers's avatar
Sietse Ringers committed
14
15
16
17
18
	"github.com/stretchr/testify/require"
)

type TestHandler struct {
	t      *testing.T
19
	c      chan *irma.SessionError
Sietse Ringers's avatar
Sietse Ringers committed
20
21
22
	client *Client
}

23
24
func (th TestHandler) KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier) {
	th.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.New("KeyshareEnrollmentIncomplete")})
25
26
}

27
func (th TestHandler) KeyshareBlocked(manager irma.SchemeManagerIdentifier, duration int) {
28
	th.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.New("KeyshareBlocked")})
29
30
}

31
func (th TestHandler) KeyshareEnrollmentMissing(manager irma.SchemeManagerIdentifier) {
32
	th.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.Errorf("Missing keyshare server %s", manager.String())})
Sietse Ringers's avatar
Sietse Ringers committed
33
34
}

35
36
37
38
func (th TestHandler) KeyshareEnrollmentDeleted(manager irma.SchemeManagerIdentifier) {
	th.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.Errorf("Keyshare enrollment deleted for %s", manager.String())})
}

39
func (th TestHandler) StatusUpdate(action irma.Action, status irma.Status) {}
40
func (th TestHandler) Success(action irma.Action, result string) {
Sietse Ringers's avatar
Sietse Ringers committed
41
42
	th.c <- nil
}
43
44
func (th TestHandler) Cancelled(action irma.Action) {
	th.c <- &irma.SessionError{}
Sietse Ringers's avatar
Sietse Ringers committed
45
}
46
func (th TestHandler) Failure(action irma.Action, err *irma.SessionError) {
Sietse Ringers's avatar
Sietse Ringers committed
47
48
49
50
51
52
	select {
	case th.c <- err:
	default:
		th.t.Fatal(err)
	}
}
53
func (th TestHandler) UnsatisfiableRequest(action irma.Action, serverName string, missing irma.AttributeDisjunctionList) {
54
55
	th.c <- &irma.SessionError{
		ErrorType: irma.ErrorType("UnsatisfiableRequest"),
Sietse Ringers's avatar
Sietse Ringers committed
56
57
	}
}
58
59
60
func (th TestHandler) RequestVerificationPermission(request irma.DisclosureRequest, ServerName string, callback PermissionHandler) {
	choice := &irma.DisclosureChoice{
		Attributes: []*irma.AttributeIdentifier{},
Sietse Ringers's avatar
Sietse Ringers committed
61
	}
62
	var candidates []*irma.AttributeIdentifier
Sietse Ringers's avatar
Sietse Ringers committed
63
64
	for _, disjunction := range request.Content {
		candidates = th.client.Candidates(disjunction)
Sietse Ringers's avatar
Sietse Ringers committed
65
66
67
		if len(candidates) == 0 {
			th.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.New("No disclosure candidates found")})
		}
Sietse Ringers's avatar
Sietse Ringers committed
68
69
70
71
		choice.Attributes = append(choice.Attributes, candidates[0])
	}
	callback(true, choice)
}
72
73
func (th TestHandler) RequestIssuancePermission(request irma.IssuanceRequest, ServerName string, callback PermissionHandler) {
	dreq := irma.DisclosureRequest{
Sietse Ringers's avatar
Sietse Ringers committed
74
75
76
77
78
		SessionRequest: request.SessionRequest,
		Content:        request.Disclose,
	}
	th.RequestVerificationPermission(dreq, ServerName, callback)
}
79
func (th TestHandler) RequestSignaturePermission(request irma.SignatureRequest, ServerName string, callback PermissionHandler) {
Sietse Ringers's avatar
Sietse Ringers committed
80
81
	th.RequestVerificationPermission(request.DisclosureRequest, ServerName, callback)
}
82
func (th TestHandler) RequestSchemeManagerPermission(manager *irma.SchemeManager, callback func(proceed bool)) {
Sietse Ringers's avatar
Sietse Ringers committed
83
84
85
86
87
88
	callback(true)
}
func (th TestHandler) RequestPin(remainingAttempts int, callback PinHandler) {
	callback(true, "12345")
}

89
90
91
func getDisclosureJwt(name string, id irma.AttributeTypeIdentifier) interface{} {
	return irma.NewServiceProviderJwt(name, &irma.DisclosureRequest{
		Content: irma.AttributeDisjunctionList([]*irma.AttributeDisjunction{{
Sietse Ringers's avatar
Sietse Ringers committed
92
			Label:      "foo",
93
			Attributes: []irma.AttributeTypeIdentifier{id},
Sietse Ringers's avatar
Sietse Ringers committed
94
95
96
97
		}}),
	})
}

98
99
func getSigningJwt(name string, id irma.AttributeTypeIdentifier) interface{} {
	return irma.NewSignatureRequestorJwt(name, &irma.SignatureRequest{
Sietse Ringers's avatar
Sietse Ringers committed
100
101
		Message:     "test",
		MessageType: "STRING",
102
103
		DisclosureRequest: irma.DisclosureRequest{
			Content: irma.AttributeDisjunctionList([]*irma.AttributeDisjunction{{
Sietse Ringers's avatar
Sietse Ringers committed
104
				Label:      "foo",
105
				Attributes: []irma.AttributeTypeIdentifier{id},
Sietse Ringers's avatar
Sietse Ringers committed
106
107
108
109
110
			}}),
		},
	})
}

111
func getIssuanceRequest(defaultValidity bool) *irma.IssuanceRequest {
112
	temp := irma.Timestamp(irma.FloorToEpochBoundary(time.Now().AddDate(1, 0, 0)))
113
	var expiry *irma.Timestamp
114
115
	credid1 := irma.NewCredentialTypeIdentifier("irma-demo.RU.studentCard")
	credid2 := irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root")
116

117
118
119
120
	if !defaultValidity {
		expiry = &temp
	}

121
	return &irma.IssuanceRequest{
122
		Credentials: []*irma.CredentialRequest{
Sietse Ringers's avatar
Sietse Ringers committed
123
			{
124
				Validity:         expiry,
Sietse Ringers's avatar
Sietse Ringers committed
125
126
127
				CredentialTypeID: &credid1,
				Attributes: map[string]string{
					"university":        "Radboud",
128
					"studentCardNumber": "31415927",
Sietse Ringers's avatar
Sietse Ringers committed
129
130
131
132
					"studentID":         "s1234567",
					"level":             "42",
				},
			}, {
133
				Validity:         expiry,
Sietse Ringers's avatar
Sietse Ringers committed
134
135
136
137
138
139
				CredentialTypeID: &credid2,
				Attributes: map[string]string{
					"BSN": "299792458",
				},
			},
		},
140
141
142
	}
}

143
144
func getNameIssuanceRequest() *irma.IssuanceRequest {
	expiry := irma.Timestamp(irma.NewMetadataAttribute(0).Expiry())
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
	credid := irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.fullName")

	req := &irma.IssuanceRequest{
		Credentials: []*irma.CredentialRequest{
			{
				Validity:         &expiry,
				CredentialTypeID: &credid,
				Attributes: map[string]string{
					"firstnames": "Johan Pieter",
					"firstname":  "Johan",
					"familyname": "Stuivezand",
				},
			},
		},
	}

	return req
}

164
165
166
167
168
169
170
func getIssuanceJwt(name string, defaultValidity bool, attribute string) interface{} {
	jwt := irma.NewIdentityProviderJwt(name, getIssuanceRequest(defaultValidity))
	if attribute != "" {
		jwt.Request.Request.Credentials[0].Attributes["studentCardNumber"] = attribute
	}
	return jwt

171
172
173
}

func getCombinedJwt(name string, id irma.AttributeTypeIdentifier) interface{} {
174
	isreq := getIssuanceRequest(false)
175
176
177
178
	isreq.Disclose = irma.AttributeDisjunctionList{
		&irma.AttributeDisjunction{Label: "foo", Attributes: []irma.AttributeTypeIdentifier{id}},
	}
	return irma.NewIdentityProviderJwt(name, isreq)
Sietse Ringers's avatar
Sietse Ringers committed
179
180
181
182
}

// StartSession starts an IRMA session by posting the request,
// and retrieving the QR contents from the specified url.
183
184
185
func StartSession(request interface{}, url string) (*irma.Qr, error) {
	server := irma.NewHTTPTransport(url)
	var response irma.Qr
Sietse Ringers's avatar
Sietse Ringers committed
186
187
188
189
190
191
192
193
	err := server.Post("", &response, request)
	if err != nil {
		return nil, err
	}
	return &response, nil
}

func TestSigningSession(t *testing.T) {
194
	id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
Sietse Ringers's avatar
Sietse Ringers committed
195
196
197
198
199
200
201
	name := "testsigclient"

	jwtcontents := getSigningJwt(name, id)
	sessionHelper(t, jwtcontents, "signature", nil)
}

func TestDisclosureSession(t *testing.T) {
202
	id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
Sietse Ringers's avatar
Sietse Ringers committed
203
204
205
206
207
208
209
	name := "testsp"

	jwtcontents := getDisclosureJwt(name, id)
	sessionHelper(t, jwtcontents, "verification", nil)
}

func TestIssuanceSession(t *testing.T) {
210
	id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
Sietse Ringers's avatar
Sietse Ringers committed
211
212
	name := "testip"

213
	jwtcontents := getCombinedJwt(name, id)
Sietse Ringers's avatar
Sietse Ringers committed
214
215
216
	sessionHelper(t, jwtcontents, "issue", nil)
}

217
218
func TestDefaultCredentialValidity(t *testing.T) {
	client := parseStorage(t)
219
	jwtcontents := getIssuanceJwt("testip", true, "")
220
221
222
	sessionHelper(t, jwtcontents, "issue", client)
}

223
func TestIssuanceOptionalEmptyAttributes(t *testing.T) {
224
225
226
227
228
229
230
231
232
	req := getNameIssuanceRequest()
	jwt := irma.NewIdentityProviderJwt("testip", req)
	sessionHelper(t, jwt, "issue", nil)
}

func TestIssuanceOptionalZeroLengthAttributes(t *testing.T) {
	req := getNameIssuanceRequest()
	req.Credentials[0].Attributes["prefix"] = ""
	jwt := irma.NewIdentityProviderJwt("testip", req)
233
234
235
236
	sessionHelper(t, jwt, "issue", nil)
}

func TestIssuanceOptionalSetAttributes(t *testing.T) {
237
238
239
	req := getNameIssuanceRequest()
	req.Credentials[0].Attributes["prefix"] = "van"
	jwt := irma.NewIdentityProviderJwt("testip", req)
240
241
242
	sessionHelper(t, jwt, "issue", nil)
}

243
244
245
246
func TestLargeAttribute(t *testing.T) {
	client := parseStorage(t)
	require.NoError(t, client.RemoveAllCredentials())

247
	jwtcontents := getIssuanceJwt("testip", false, "1234567890123456789012345678901234567890") // 40 chars
248
249
250
251
252
253
254
255
256
	sessionHelper(t, jwtcontents, "issue", client)

	cred, err := client.credential(irma.NewCredentialTypeIdentifier("irma-demo.RU.studentCard"), 0)
	require.NoError(t, err)
	require.True(t, cred.Signature.Verify(cred.Pk, cred.Attributes))

	jwtcontents = getDisclosureJwt("testsp", irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.university"))
	sessionHelper(t, jwtcontents, "verification", client)

257
	test.ClearTestStorage(t)
258
259
}

260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
/* There is an annoying difference between how Java and Go convert big integers to and from
byte arrays: in Java the sign of the integer is taken into account, but not in Go. This means
that in Java, when converting a bigint to or from a byte array, the most significant bit
indicates the sign of the integer. In Go this is not the case. This resulted in invalid
signatures being issued in the issuance protocol in two distinct ways, of which we test here
that they have been fixed. */
func TestAttributeByteEncoding(t *testing.T) {
	client := parseStorage(t)
	require.NoError(t, client.RemoveAllCredentials())

	/* After bitshifting the presence bit into the large attribute below, the most significant
	bit is 1. In the bigint->[]byte conversion that happens before hashing this attribute, in
	Java this results in an extra 0 byte being prepended in order to have a 0 instead as most
	significant (sign) bit. We test that the Java implementation correctly removes the extraneous
	0 byte. */
	jwtcontents := getIssuanceJwt("testip", false, "a23456789012345678901234567890")
	sessionHelper(t, jwtcontents, "issue", client)

	/* After converting the attribute below to bytes (using UTF8, on which Java and Go do agree),
	the most significant bit of the byte version of this attribute is 1. In the []byte->bigint
	conversion that happens at that point in the Java implementation (bitshifting is done
	afterwards), this results in a negative number in Java and a positive number in Go. We test
	here that the Java correctly prepends a 0 byte just before this conversion in order to get
	the same positive bigint. */
	jwtcontents = getIssuanceJwt("testip", false, "é")
	sessionHelper(t, jwtcontents, "issue", client)
}

Sietse Ringers's avatar
Sietse Ringers committed
288
func sessionHelper(t *testing.T, jwtcontents interface{}, url string, client *Client) {
289
290
291
292
	sessionHandlerHelper(t, jwtcontents, url, client, nil)
}

func sessionHandlerHelper(t *testing.T, jwtcontents interface{}, url string, client *Client, h Handler) {
Sietse Ringers's avatar
Sietse Ringers committed
293
294
295
296
297
	init := client == nil
	if init {
		client = parseStorage(t)
	}

298
	url = "http://localhost:8088/irma_api_server/api/v2/" + url
Sietse Ringers's avatar
Sietse Ringers committed
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
	//url = "https://demo.irmacard.org/tomcat/irma_api_server/api/v2/" + url

	headerbytes, err := json.Marshal(&map[string]string{"alg": "none", "typ": "JWT"})
	require.NoError(t, err)
	bodybytes, err := json.Marshal(jwtcontents)
	require.NoError(t, err)

	jwt := base64.RawStdEncoding.EncodeToString(headerbytes) + "." + base64.RawStdEncoding.EncodeToString(bodybytes) + "."
	qr, transportErr := StartSession(jwt, url)
	if transportErr != nil {
		fmt.Printf("+%v\n", transportErr)
	}
	require.NoError(t, transportErr)
	qr.URL = url + "/" + qr.URL

314
	c := make(chan *irma.SessionError)
315
316
317
318
	if h == nil {
		h = TestHandler{t, c, client}
	}
	client.NewSession(qr, h)
Sietse Ringers's avatar
Sietse Ringers committed
319
320
321
322
323
324

	if err := <-c; err != nil {
		t.Fatal(*err)
	}

	if init {
325
		test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
326
327
328
	}
}

329
func keyshareSessions(t *testing.T, client *Client) {
330
	id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
331
	expiry := irma.Timestamp(irma.NewMetadataAttribute(0).Expiry())
332
	credid := irma.NewCredentialTypeIdentifier("test.test.mijnirma")
333
	jwt := getCombinedJwt("testip", id)
334
335
336
	jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials = append(
		jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials,
		&irma.CredentialRequest{
Sietse Ringers's avatar
Sietse Ringers committed
337
338
			Validity:         &expiry,
			CredentialTypeID: &credid,
339
			Attributes:       map[string]string{"email": "testusername"},
Sietse Ringers's avatar
Sietse Ringers committed
340
341
342
343
344
		},
	)
	sessionHelper(t, jwt, "issue", client)

	jwt = getDisclosureJwt("testsp", id)
345
346
347
	jwt.(*irma.ServiceProviderJwt).Request.Request.Content = append(
		jwt.(*irma.ServiceProviderJwt).Request.Request.Content,
		&irma.AttributeDisjunction{
Sietse Ringers's avatar
Sietse Ringers committed
348
			Label:      "foo",
349
			Attributes: []irma.AttributeTypeIdentifier{irma.NewAttributeTypeIdentifier("test.test.mijnirma.email")},
Sietse Ringers's avatar
Sietse Ringers committed
350
351
352
353
354
		},
	)
	sessionHelper(t, jwt, "verification", client)

	jwt = getSigningJwt("testsigclient", id)
355
356
357
	jwt.(*irma.SignatureRequestorJwt).Request.Request.Content = append(
		jwt.(*irma.SignatureRequestorJwt).Request.Request.Content,
		&irma.AttributeDisjunction{
Sietse Ringers's avatar
Sietse Ringers committed
358
			Label:      "foo",
359
			Attributes: []irma.AttributeTypeIdentifier{irma.NewAttributeTypeIdentifier("test.test.mijnirma.email")},
Sietse Ringers's avatar
Sietse Ringers committed
360
361
362
		},
	)
	sessionHelper(t, jwt, "signature", client)
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
}

// Enroll at a keyshare server and do an issuance, disclosure,
// and issuance session, also using irma-demo credentials deserialized from Android storage
func TestKeyshareEnrollmentAndSessions(t *testing.T) {
	client := parseStorage(t)
	credtype := irma.NewCredentialTypeIdentifier("test.test.mijnirma")

	// Remove existing registration at test keyshare server
	require.NoError(t, client.RemoveCredentialByHash(
		client.Attributes(credtype, 0).Hash(),
	))
	require.NoError(t, client.KeyshareRemove(irma.NewSchemeManagerIdentifier("test")))

	// Do a new registration session
	c := make(chan error) // channel for TestClientHandler to inform us of result
	client.handler.(*TestClientHandler).c = c
	bytes := make([]byte, 8, 8)
	rand.Read(bytes)
Sietse Ringers's avatar
Sietse Ringers committed
382
	require.NoError(t, client.keyshareEnrollWorker(irma.NewSchemeManagerIdentifier("test"), nil, "12345", "en"))
383
384
385
386
387
388
389
	if err := <-c; err != nil {
		t.Fatal(err)
	}

	require.NotNil(t, client.Attributes(credtype, 0))

	keyshareSessions(t, client)
Sietse Ringers's avatar
Sietse Ringers committed
390

391
	test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
392
393
394
395
396
397
398
399
}

// Use the existing keyshare enrollment and credentials deserialized from Android storage
// in a keyshare session of each session type.
// Use keyshareuser.sql to enroll the user at the keyshare server.
func TestKeyshareSessions(t *testing.T) {
	client := parseStorage(t)

400
	keyshareSessions(t, client)
Sietse Ringers's avatar
Sietse Ringers committed
401

402
	test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
403
}