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

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
214
	name := "testsp"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Sietse Ringers's avatar
Sietse Ringers committed
374
375
	require.NoError(t, client.keyshareChangePinWorker(irma.NewSchemeManagerIdentifier("test"), "12345", "54321"))
	require.NoError(t, client.keyshareChangePinWorker(irma.NewSchemeManagerIdentifier("test"), "54321", "12345"))
376
377
378
379

	test.ClearTestStorage(t)
}

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

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

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

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

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