irmago_test.go 11.4 KB
Newer Older
Sietse Ringers's avatar
Sietse Ringers committed
1
package irmago
2
3

import (
4
5
	"fmt"
	"math/big"
Sietse Ringers's avatar
Sietse Ringers committed
6
	"os"
7
8
	"testing"
	"time"
Sietse Ringers's avatar
Sietse Ringers committed
9

10
11
	"encoding/json"

Sietse Ringers's avatar
Sietse Ringers committed
12
	"github.com/mhe/gabi"
13
	"github.com/stretchr/testify/assert"
Sietse Ringers's avatar
Sietse Ringers committed
14
	"github.com/stretchr/testify/require"
15
16
)

Sietse Ringers's avatar
Sietse Ringers committed
17
func TestMain(m *testing.M) {
Sietse Ringers's avatar
Sietse Ringers committed
18
19
	retCode := m.Run()

Sietse Ringers's avatar
Sietse Ringers committed
20
21
	err := os.RemoveAll("testdata/storage/test")
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
22
		fmt.Println("Could not delete test storage")
Sietse Ringers's avatar
Sietse Ringers committed
23
24
25
		os.Exit(1)
	}

Sietse Ringers's avatar
Sietse Ringers committed
26
27
	os.Exit(retCode)
}
Sietse Ringers's avatar
Sietse Ringers committed
28

Sietse Ringers's avatar
Sietse Ringers committed
29
30
type IgnoringKeyshareHandler struct{}

31
func (i *IgnoringKeyshareHandler) StartRegistration(m *SchemeManager, callback func(e, p string)) {
Sietse Ringers's avatar
Sietse Ringers committed
32
}
Sietse Ringers's avatar
Sietse Ringers committed
33

34
func parseStorage(t *testing.T) *CredentialManager {
35
	exists, err := PathExists("testdata/storage/test")
Sietse Ringers's avatar
Sietse Ringers committed
36
37
38
	require.NoError(t, err, "pathexists() failed")
	if !exists {
		require.NoError(t, os.Mkdir("testdata/storage/test", 0755), "Could not create test storage")
Sietse Ringers's avatar
Sietse Ringers committed
39
	}
40
	manager, err := NewCredentialManager(
41
42
43
		"testdata/storage/test",
		"testdata/irma_configuration",
		&IgnoringKeyshareHandler{},
44
45
46
	)
	require.NoError(t, err)
	return manager
Sietse Ringers's avatar
Sietse Ringers committed
47
48
49
}

func teardown(t *testing.T) {
50
	assert.NoError(t, os.RemoveAll("testdata/storage/test"))
Sietse Ringers's avatar
Sietse Ringers committed
51
52
}

53
54
55
56
57
58
59
// A convenience function for initializing big integers from known correct (10
// base) strings. Use with care, errors are ignored.
func s2big(s string) (r *big.Int) {
	r, _ = new(big.Int).SetString(s, 10)
	return
}

60
61
func parseAndroidStorage(t *testing.T, manager *CredentialManager) {
	assert.NoError(t, manager.ParseAndroidStorage(), "ParseAndroidStorage() failed")
Sietse Ringers's avatar
Sietse Ringers committed
62
63
}

64
65
func verifyManagerIsUnmarshaled(t *testing.T, manager *CredentialManager) {
	cred, err := manager.credential(NewCredentialTypeIdentifier("irma-demo.RU.studentCard"), 0)
66
67
68
	assert.NoError(t, err, "could not fetch credential")
	assert.NotNil(t, cred, "Credential should exist")
	assert.NotNil(t, cred.Attributes[0], "Metadata attribute of irma-demo.RU.studentCard should not be nil")
69

Sietse Ringers's avatar
Sietse Ringers committed
70
	cred, err = manager.credential(NewCredentialTypeIdentifier("test.test.mijnirma"), 0)
71
72
73
74
	assert.NoError(t, err, "could not fetch credential")
	assert.NotNil(t, cred, "Credential should exist")
	assert.NotNil(t, cred.Signature.KeyshareP)

Sietse Ringers's avatar
Sietse Ringers committed
75
	assert.NotEmpty(t, manager.CredentialInfoList())
76

77
	assert.True(t,
Sietse Ringers's avatar
Sietse Ringers committed
78
		cred.Signature.Verify(cred.PublicKey(), cred.Attributes),
79
80
81
		"Credential should be valid",
	)
}
Sietse Ringers's avatar
Sietse Ringers committed
82

Sietse Ringers's avatar
Sietse Ringers committed
83
84
func verifyCredentials(t *testing.T, manager *CredentialManager) {
	for credtype, credsmap := range manager.credentials {
85
86
87
88
89
		for index, cred := range credsmap {
			require.True(t,
				cred.Credential.Signature.Verify(cred.PublicKey(), cred.Attributes),
				"Credential %s-%d was invalid", credtype.String(), index,
			)
Sietse Ringers's avatar
Sietse Ringers committed
90
			require.Equal(t, cred.Attributes[0], manager.secretkey,
91
92
93
94
95
				"Secret key of credential %s-%d unequal to main secret key")
		}
	}
}

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
func verifyPaillierKey(t *testing.T, PrivateKey *paillierPrivateKey) {
	require.NotNil(t, PrivateKey)
	require.NotNil(t, PrivateKey.L)
	require.NotNil(t, PrivateKey.U)
	require.NotNil(t, PrivateKey.PublicKey.N)

	require.Equal(t, big.NewInt(1), new(big.Int).Exp(big.NewInt(2), PrivateKey.L, PrivateKey.N))
	require.Equal(t, PrivateKey.NSquared, new(big.Int).Exp(PrivateKey.N, big.NewInt(2), nil))

	plaintext := "Hello Paillier!"
	ciphertext, err := PrivateKey.Encrypt([]byte(plaintext))
	require.NoError(t, err)
	decrypted, err := PrivateKey.Decrypt(ciphertext)
	require.NoError(t, err)
	require.Equal(t, plaintext, string(decrypted))
}

113
114
115
func verifyKeyshareIsUnmarshaled(t *testing.T, manager *CredentialManager) {
	require.NotNil(t, manager.paillierKeyCache)
	require.NotNil(t, manager.keyshareServers)
116
	test := NewSchemeManagerIdentifier("test")
117
118
	require.Contains(t, manager.keyshareServers, test)
	kss := manager.keyshareServers[test]
119
120
121
	require.NotEmpty(t, kss.Nonce)

	verifyPaillierKey(t, kss.PrivateKey)
122
	verifyPaillierKey(t, manager.paillierKeyCache)
123
124
}

125
126
func verifyStoreIsLoaded(t *testing.T, store *ConfigurationStore) {
	assert.NotNil(t, store.Issuers[NewIssuerIdentifier("irma-demo.RU")].CurrentPublicKey(store).N, "irma-demo.RU public key has no modulus")
127
	assert.Equal(t,
128
		"Irma Demo",
129
		store.SchemeManagers[NewSchemeManagerIdentifier("irma-demo")].Name["en"],
130
		"irma-demo scheme manager has unexpected name")
131
	assert.Equal(t,
132
		"Radboud Universiteit Nijmegen",
133
		store.Issuers[NewIssuerIdentifier("irma-demo.RU")].Name["en"],
134
		"irma-demo.RU issuer has unexpected name")
135
	assert.Equal(t,
136
		"Student Card",
137
		store.Credentials[NewCredentialTypeIdentifier("irma-demo.RU.studentCard")].ShortName["en"],
138
139
		"irma-demo.RU.studentCard has unexpected name")

140
	assert.Equal(t,
141
		"studentID",
142
		store.Credentials[NewCredentialTypeIdentifier("irma-demo.RU.studentCard")].Attributes[2].ID,
143
144
145
146
		"irma-demo.RU.studentCard.studentID has unexpected name")

	// Hash algorithm pseudocode:
	// Base64(SHA256("irma-demo.RU.studentCard")[0:16])
147
	assert.Contains(t, store.reverseHashes, "1stqlPad5edpfS1Na1U+DA==",
148
		"irma-demo.RU.studentCard had improper hash")
149
	assert.Contains(t, store.reverseHashes, "CLjnADMBYlFcuGOT7Z0xRg==",
150
		"irma-demo.MijnOverheid.root had improper hash")
151
152
153
}

func TestAndroidParse(t *testing.T) {
154
	manager := parseStorage(t)
155
	verifyStoreIsLoaded(t, manager.Store)
156

157
158
	parseAndroidStorage(t, manager)
	verifyManagerIsUnmarshaled(t, manager)
Sietse Ringers's avatar
Sietse Ringers committed
159
	verifyCredentials(t, manager)
160
	verifyKeyshareIsUnmarshaled(t, manager)
161
162
163
164
165

	teardown(t)
}

func TestUnmarshaling(t *testing.T) {
166
167
	manager := parseStorage(t)
	parseAndroidStorage(t, manager)
168

169
	newmanager, err := NewCredentialManager("testdata/storage/test", "testdata/irma_configuration", nil)
170
171
	require.NoError(t, err)

172
	verifyManagerIsUnmarshaled(t, newmanager)
Sietse Ringers's avatar
Sietse Ringers committed
173
	verifyCredentials(t, newmanager)
174
	verifyKeyshareIsUnmarshaled(t, newmanager)
Sietse Ringers's avatar
Sietse Ringers committed
175
176

	teardown(t)
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
}

func TestMetadataAttribute(t *testing.T) {
	metadata := NewMetadataAttribute()
	if metadata.Version() != 0x02 {
		t.Errorf("Unexpected metadata version: %d", metadata.Version())
	}

	expiry := metadata.SigningDate().Unix() + int64(metadata.ValidityDuration()*ExpiryFactor)
	if !time.Unix(expiry, 0).Equal(metadata.Expiry()) {
		t.Errorf("Invalid signing date")
	}

	if metadata.KeyCounter() != 0 {
		t.Errorf("Unexpected key counter")
	}
}

func TestMetadataCompatibility(t *testing.T) {
Sietse Ringers's avatar
Sietse Ringers committed
196
	store := NewConfigurationStore()
Sietse Ringers's avatar
Sietse Ringers committed
197
	require.NoError(t, store.ParseFolder("testdata/irma_configuration"))
198
199

	// An actual metadata attribute of an IRMA credential extracted from the IRMA app
Sietse Ringers's avatar
Sietse Ringers committed
200
	attr := MetadataFromInt(s2big("49043481832371145193140299771658227036446546573739245068"), store)
201
	assert.NotNil(t, attr.CredentialType(), "attr.CredentialType() should not be nil")
202

203
	assert.Equal(t,
204
		NewCredentialTypeIdentifier("irma-demo.RU.studentCard"),
205
206
207
		attr.CredentialType().Identifier(),
		"Metadata credential type was not irma-demo.RU.studentCard",
	)
208
209
210
211
	assert.Equal(t, byte(0x02), attr.Version(), "Unexpected metadata version")
	assert.Equal(t, time.Unix(1499904000, 0), attr.SigningDate(), "Unexpected signing date")
	assert.Equal(t, time.Unix(1516233600, 0), attr.Expiry(), "Unexpected expiry date")
	assert.Equal(t, 2, attr.KeyCounter(), "Unexpected key counter")
Sietse Ringers's avatar
Sietse Ringers committed
212
213

	teardown(t)
214
}
215
216

func TestAttributeDisjunctionMarshaling(t *testing.T) {
Sietse Ringers's avatar
Sietse Ringers committed
217
	store := NewConfigurationStore()
Sietse Ringers's avatar
Sietse Ringers committed
218
	store.ParseFolder("testdata/irma_configuration")
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
	disjunction := AttributeDisjunction{}

	var _ json.Unmarshaler = &disjunction
	var _ json.Marshaler = &disjunction

	id := NewAttributeTypeIdentifier("MijnOverheid.ageLower.over18")

	attrsjson := `
	{
		"label": "Over 18",
		"attributes": {
			"MijnOverheid.ageLower.over18": "yes",
			"Thalia.age.over18": "Yes"
		}
	}`
	require.NoError(t, json.Unmarshal([]byte(attrsjson), &disjunction))
	require.True(t, disjunction.HasValues())
	require.Contains(t, disjunction.Attributes, id)
	require.Contains(t, disjunction.Values, id)
	require.Equal(t, disjunction.Values[id], "yes")

	disjunction = AttributeDisjunction{}
	attrsjson = `
	{
		"label": "Over 18",
		"attributes": [
			"MijnOverheid.ageLower.over18",
			"Thalia.age.over18"
		]
	}`
	require.NoError(t, json.Unmarshal([]byte(attrsjson), &disjunction))
	require.False(t, disjunction.HasValues())
	require.Contains(t, disjunction.Attributes, id)

Sietse Ringers's avatar
Sietse Ringers committed
253
	require.True(t, disjunction.MatchesStore(store))
254
255
256
257
258

	require.False(t, disjunction.Satisfied())
	disjunction.selected = &disjunction.Attributes[0]
	require.True(t, disjunction.Satisfied())
}
259
260

func TestCandidates(t *testing.T) {
261
262
	manager := parseStorage(t)
	parseAndroidStorage(t, manager)
263
264
265
266
267

	attrtype := NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
	disjunction := &AttributeDisjunction{
		Attributes: []AttributeTypeIdentifier{attrtype},
	}
268
	attrs := manager.Candidates(disjunction)
269
270
271
272
273
274
275
276
277
	require.NotNil(t, attrs)
	require.Len(t, attrs, 1)

	attr := attrs[0]
	require.NotNil(t, attr)
	require.Equal(t, attr.Type, attrtype)

	disjunction = &AttributeDisjunction{
		Attributes: []AttributeTypeIdentifier{attrtype},
278
		Values:     map[AttributeTypeIdentifier]string{attrtype: "456"},
279
	}
280
	attrs = manager.Candidates(disjunction)
281
282
283
284
285
286
287
	require.NotNil(t, attrs)
	require.Len(t, attrs, 1)

	disjunction = &AttributeDisjunction{
		Attributes: []AttributeTypeIdentifier{attrtype},
		Values:     map[AttributeTypeIdentifier]string{attrtype: "foobarbaz"},
	}
288
	attrs = manager.Candidates(disjunction)
289
290
291
292
293
	require.NotNil(t, attrs)
	require.Empty(t, attrs)

	teardown(t)
}
294
295

func TestTimestamp(t *testing.T) {
296
297
	mytime := Timestamp(time.Unix(1500000000, 0))
	timestruct := struct{ Time *Timestamp }{Time: &mytime}
298
299
300
	bytes, err := json.Marshal(timestruct)
	require.NoError(t, err)

301
	timestruct = struct{ Time *Timestamp }{}
302
303
304
	require.NoError(t, json.Unmarshal(bytes, &timestruct))
	require.Equal(t, time.Time(*timestruct.Time).Unix(), int64(1500000000))
}
Sietse Ringers's avatar
Sietse Ringers committed
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348

func TestServiceProvider(t *testing.T) {
	var spjwt ServiceProviderJwt

	var spjson = `{
		"sprequest": {
			"validity": 60,
			"timeout": 60,
			"request": {
				"content": [
					{
						"label": "ID",
						"attributes": ["irma-demo.RU.studentCard.studentID"]
					}
				]
			}
		}
	}`

	require.NoError(t, json.Unmarshal([]byte(spjson), &spjwt))
	require.NotNil(t, spjwt.Request.Request.Content)
	require.NotEmpty(t, spjwt.Request.Request.Content)
	require.NotNil(t, spjwt.Request.Request.Content[0])
	require.NotEmpty(t, spjwt.Request.Request.Content[0])
	require.NotNil(t, spjwt.Request.Request.Content[0].Attributes)
	require.NotEmpty(t, spjwt.Request.Request.Content[0].Attributes)
	require.Equal(t, spjwt.Request.Request.Content[0].Attributes[0].Name(), "studentID")

	require.NotNil(t, spjwt.Request.Request.Content.Find(NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")))
}

func TestTransport(t *testing.T) {
	transport := NewHTTPTransport("https://xkcd.com")
	obj := &struct {
		Num   int    `json:"num"`
		Img   string `json:"img"`
		Title string `json:"title"`
	}{}

	err := transport.Get("614/info.0.json", obj)
	if err != nil { // require.NoError() does not work because of the type of err
		t.Fatalf("%+v\n", err)
	}
}
Sietse Ringers's avatar
Sietse Ringers committed
349
350

func TestPaillier(t *testing.T) {
351
352
	manager := parseStorage(t)
	parseAndroidStorage(t, manager)
Sietse Ringers's avatar
Sietse Ringers committed
353
354
355
356
357

	challenge, _ := gabi.RandomBigInt(256)
	comm, _ := gabi.RandomBigInt(1000)
	resp, _ := gabi.RandomBigInt(1000)

358
	sk := manager.paillierKey(true)
Sietse Ringers's avatar
Sietse Ringers committed
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
	bytes, err := sk.Encrypt(challenge.Bytes())
	require.NoError(t, err)
	cipher := new(big.Int).SetBytes(bytes)

	bytes, err = sk.Encrypt(comm.Bytes())
	require.NoError(t, err)
	commcipher := new(big.Int).SetBytes(bytes)

	// [[ c ]]^resp * [[ comm ]]
	cipher.Exp(cipher, resp, sk.NSquared).Mul(cipher, commcipher).Mod(cipher, sk.NSquared)

	bytes, err = sk.Decrypt(cipher.Bytes())
	require.NoError(t, err)
	plaintext := new(big.Int).SetBytes(bytes)
	expected := new(big.Int).Set(challenge)
	expected.Mul(expected, resp).Add(expected, comm)

	require.Equal(t, plaintext, expected)

	teardown(t)
}