session_test.go 14 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
	"github.com/stretchr/testify/require"
15
	"math/big"
Sietse Ringers's avatar
Sietse Ringers committed
16
17
18
19
)

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

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

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

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

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

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

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

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

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

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

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

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

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

175
176
177
}

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

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

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

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

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

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

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

217
	jwtcontents := getCombinedJwt(name, id)
Sietse Ringers's avatar
Sietse Ringers committed
218
219
220
	sessionHelper(t, jwtcontents, "issue", nil)
}

221
222
func TestDefaultCredentialValidity(t *testing.T) {
	client := parseStorage(t)
223
	jwtcontents := getIssuanceJwt("testip", true, "")
224
225
226
	sessionHelper(t, jwtcontents, "issue", client)
}

227
func TestIssuanceOptionalEmptyAttributes(t *testing.T) {
228
229
230
231
232
233
234
235
236
	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)
237
238
239
240
	sessionHelper(t, jwt, "issue", nil)
}

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

247
248
249
250
func TestLargeAttribute(t *testing.T) {
	client := parseStorage(t)
	require.NoError(t, client.RemoveAllCredentials())

251
	jwtcontents := getIssuanceJwt("testip", false, "1234567890123456789012345678901234567890") // 40 chars
252
253
254
255
256
257
258
259
260
	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)

261
	test.ClearTestStorage(t)
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
288
289
290
291
/* 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
292
func sessionHelper(t *testing.T, jwtcontents interface{}, url string, client *Client) {
293
294
295
296
	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
297
298
299
300
301
	init := client == nil
	if init {
		client = parseStorage(t)
	}

302
	url = "http://localhost:8088/irma_api_server/api/v2/" + url
Sietse Ringers's avatar
Sietse Ringers committed
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
	//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

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

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

	if init {
329
		test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
330
331
332
	}
}

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

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

	jwt = getSigningJwt("testsigclient", id)
359
360
361
	jwt.(*irma.SignatureRequestorJwt).Request.Request.Content = append(
		jwt.(*irma.SignatureRequestorJwt).Request.Request.Content,
		&irma.AttributeDisjunction{
Sietse Ringers's avatar
Sietse Ringers committed
362
			Label:      "foo",
363
			Attributes: []irma.AttributeTypeIdentifier{irma.NewAttributeTypeIdentifier("test.test.mijnirma.email")},
Sietse Ringers's avatar
Sietse Ringers committed
364
365
366
		},
	)
	sessionHelper(t, jwt, "signature", client)
367
368
}

369
// Test pinchange interaction
370
func TestKeyshareChangePin(t *testing.T) {
371
372
	client := parseStorage(t)

373
374
	require.NoError(t, client.keyshareChangePinWorker(irma.NewSchemeManagerIdentifier("test"), "12345", "54321"));
	require.NoError(t, client.keyshareChangePinWorker(irma.NewSchemeManagerIdentifier("test"), "54321", "12345"));
375
376
377
378

	test.ClearTestStorage(t)
}

379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
// 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
396
	require.NoError(t, client.keyshareEnrollWorker(irma.NewSchemeManagerIdentifier("test"), nil, "12345", "en"))
397
398
399
400
401
402
403
	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
404

405
	test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
406
407
408
409
410
411
412
413
}

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

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

416
	test.ClearTestStorage(t)
Sietse Ringers's avatar
Sietse Ringers committed
417
}