session_test.go 14.7 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

Sietse Ringers's avatar
Sietse Ringers committed
11
12
	"math/big"

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

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

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

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

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

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

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

91
92
93
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
94
			Label:      "foo",
95
			Attributes: []irma.AttributeTypeIdentifier{id},
Sietse Ringers's avatar
Sietse Ringers committed
96
97
98
99
		}}),
	})
}

100
101
func getSigningJwt(name string, id irma.AttributeTypeIdentifier) interface{} {
	return irma.NewSignatureRequestorJwt(name, &irma.SignatureRequest{
102
		Message: "test",
103
		DisclosureRequest: irma.DisclosureRequest{
104
105
106
107
			SessionRequest: irma.SessionRequest{
				Nonce:   big.NewInt(1),
				Context: big.NewInt(1),
			},
108
			Content: irma.AttributeDisjunctionList([]*irma.AttributeDisjunction{{
Sietse Ringers's avatar
Sietse Ringers committed
109
				Label:      "foo",
110
				Attributes: []irma.AttributeTypeIdentifier{id},
Sietse Ringers's avatar
Sietse Ringers committed
111
112
113
114
115
			}}),
		},
	})
}

116
func getIssuanceRequest(defaultValidity bool) *irma.IssuanceRequest {
117
	temp := irma.Timestamp(irma.FloorToEpochBoundary(time.Now().AddDate(1, 0, 0)))
118
	var expiry *irma.Timestamp
119
120
	credid1 := irma.NewCredentialTypeIdentifier("irma-demo.RU.studentCard")
	credid2 := irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root")
121

122
123
124
125
	if !defaultValidity {
		expiry = &temp
	}

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

148
149
func getNameIssuanceRequest() *irma.IssuanceRequest {
	expiry := irma.Timestamp(irma.NewMetadataAttribute(0).Expiry())
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
	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
}

169
170
171
172
173
174
175
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

176
177
178
}

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

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

func TestSigningSession(t *testing.T) {
199
	id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
Sietse Ringers's avatar
Sietse Ringers committed
200
201
202
203
204
205
206
	name := "testsigclient"

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

func TestDisclosureSession(t *testing.T) {
207
	id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
Sietse Ringers's avatar
Sietse Ringers committed
208
209
210
211
212
213
	name := "testsp"

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

214
215
216
217
218
219
220
221
func TestNoAttributeDisclosureSession(t *testing.T) {
	id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard")
	name := "testsp"

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

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

226
	jwtcontents := getCombinedJwt(name, id)
Sietse Ringers's avatar
Sietse Ringers committed
227
228
229
	sessionHelper(t, jwtcontents, "issue", nil)
}

230
231
func TestDefaultCredentialValidity(t *testing.T) {
	client := parseStorage(t)
232
	jwtcontents := getIssuanceJwt("testip", true, "")
233
234
235
	sessionHelper(t, jwtcontents, "issue", client)
}

236
func TestIssuanceOptionalEmptyAttributes(t *testing.T) {
237
238
239
240
241
242
243
244
245
	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)
246
247
248
249
	sessionHelper(t, jwt, "issue", nil)
}

func TestIssuanceOptionalSetAttributes(t *testing.T) {
250
251
252
	req := getNameIssuanceRequest()
	req.Credentials[0].Attributes["prefix"] = "van"
	jwt := irma.NewIdentityProviderJwt("testip", req)
253
254
255
	sessionHelper(t, jwt, "issue", nil)
}

256
257
258
259
func TestLargeAttribute(t *testing.T) {
	client := parseStorage(t)
	require.NoError(t, client.RemoveAllCredentials())

260
	jwtcontents := getIssuanceJwt("testip", false, "1234567890123456789012345678901234567890") // 40 chars
261
262
263
264
265
266
267
268
269
	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)

270
	test.ClearTestStorage(t)
271
272
}

273
274
275
276
277
278
279
280
281
282
283
284
285
286
func TestIssuanceSingletonCredential(t *testing.T) {
	client := parseStorage(t)
	jwtcontents := getIssuanceJwt("testip", true, "")
	credid := irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root")

	require.Len(t, client.attrs(credid), 0)

	sessionHelper(t, jwtcontents, "issue", client)
	require.Len(t, client.attrs(credid), 1)

	sessionHelper(t, jwtcontents, "issue", client)
	require.Len(t, client.attrs(credid), 1)
}

287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/* 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
315
func sessionHelper(t *testing.T, jwtcontents interface{}, url string, client *Client) {
316
317
318
319
	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
320
321
322
323
324
	init := client == nil
	if init {
		client = parseStorage(t)
	}

325
	url = "http://localhost:8088/irma_api_server/api/v2/" + url
Sietse Ringers's avatar
Sietse Ringers committed
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
	//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

341
	c := make(chan *irma.SessionError)
342
343
344
345
	if h == nil {
		h = TestHandler{t, c, client}
	}
	client.NewSession(qr, h)
Sietse Ringers's avatar
Sietse Ringers committed
346
347
348
349
350
351

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

	if init {
352
		test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
353
354
355
	}
}

356
func keyshareSessions(t *testing.T, client *Client) {
357
	id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
358
	expiry := irma.Timestamp(irma.NewMetadataAttribute(0).Expiry())
359
	credid := irma.NewCredentialTypeIdentifier("test.test.mijnirma")
360
	jwt := getCombinedJwt("testip", id)
361
362
363
	jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials = append(
		jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials,
		&irma.CredentialRequest{
Sietse Ringers's avatar
Sietse Ringers committed
364
365
			Validity:         &expiry,
			CredentialTypeID: &credid,
366
			Attributes:       map[string]string{"email": "testusername"},
Sietse Ringers's avatar
Sietse Ringers committed
367
368
369
370
371
		},
	)
	sessionHelper(t, jwt, "issue", client)

	jwt = getDisclosureJwt("testsp", id)
372
373
374
	jwt.(*irma.ServiceProviderJwt).Request.Request.Content = append(
		jwt.(*irma.ServiceProviderJwt).Request.Request.Content,
		&irma.AttributeDisjunction{
Sietse Ringers's avatar
Sietse Ringers committed
375
			Label:      "foo",
376
			Attributes: []irma.AttributeTypeIdentifier{irma.NewAttributeTypeIdentifier("test.test.mijnirma.email")},
Sietse Ringers's avatar
Sietse Ringers committed
377
378
379
380
381
		},
	)
	sessionHelper(t, jwt, "verification", client)

	jwt = getSigningJwt("testsigclient", id)
382
383
384
	jwt.(*irma.SignatureRequestorJwt).Request.Request.Content = append(
		jwt.(*irma.SignatureRequestorJwt).Request.Request.Content,
		&irma.AttributeDisjunction{
Sietse Ringers's avatar
Sietse Ringers committed
385
			Label:      "foo",
386
			Attributes: []irma.AttributeTypeIdentifier{irma.NewAttributeTypeIdentifier("test.test.mijnirma.email")},
Sietse Ringers's avatar
Sietse Ringers committed
387
388
389
		},
	)
	sessionHelper(t, jwt, "signature", client)
390
391
}

392
// Test pinchange interaction
393
func TestKeyshareChangePin(t *testing.T) {
394
395
	client := parseStorage(t)

Sietse Ringers's avatar
Sietse Ringers committed
396
397
	require.NoError(t, client.keyshareChangePinWorker(irma.NewSchemeManagerIdentifier("test"), "12345", "54321"))
	require.NoError(t, client.keyshareChangePinWorker(irma.NewSchemeManagerIdentifier("test"), "54321", "12345"))
398
399
400
401

	test.ClearTestStorage(t)
}

402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
// 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
419
	require.NoError(t, client.keyshareEnrollWorker(irma.NewSchemeManagerIdentifier("test"), nil, "12345", "en"))
420
421
422
423
424
425
426
	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
427

428
	test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
429
430
431
432
433
434
435
436
}

// 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)

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

439
	test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
440
}