irmaconfig.go 16.1 KB
Newer Older
1
package irma
2
3
4
5
6
7
8
9
10

import (
	"encoding/base64"
	"encoding/xml"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"

11
12
	"crypto/sha256"

13
14
15
16
	"fmt"

	"strings"

17
18
19
20
21
22
23
24
25
26
27
28
	"sort"

	"bytes"

	"encoding/hex"

	"crypto/ecdsa"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
	"math/big"

29
	"github.com/credentials/irmago/internal/fs"
30
	"github.com/go-errors/errors"
31
32
33
	"github.com/mhe/gabi"
)

34
// Configuration keeps track of scheme managers, issuers, credential types and public keys,
35
// dezerializing them from an irma_configuration folder, and downloads and saves new ones on demand.
36
type Configuration struct {
37
38
39
	SchemeManagers  map[SchemeManagerIdentifier]*SchemeManager
	Issuers         map[IssuerIdentifier]*Issuer
	CredentialTypes map[CredentialTypeIdentifier]*CredentialType
40

41
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
42
	reverseHashes map[string]CredentialTypeIdentifier
43
	path          string
44
	initialized   bool
45
46
}

47
48
49
50
type ConfigurationFileHash []byte

type SchemeManagerIndex map[string]ConfigurationFileHash

51
// NewConfiguration returns a new configuration. After this
52
// ParseFolder() should be called to parse the specified path.
53
54
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
	conf = &Configuration{
55
		path: path,
56
	}
57

58
	if err = fs.EnsureDirectoryExists(conf.path); err != nil {
59
60
		return nil, err
	}
61
	if assets != "" {
62
		if err = conf.Copy(assets, false); err != nil {
63
64
65
66
			return nil, err
		}
	}

67
68
69
	return
}

70
// ParseFolder populates the current Configuration by parsing the storage path,
71
// listing the containing scheme managers, issuers and credential types.
72
func (conf *Configuration) ParseFolder() error {
73
	// Init all maps
74
75
76
77
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
78

79
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
80

81
	err := iterateSubfolders(conf.path, func(dir string) error {
82
		manager := &SchemeManager{}
83
		if err := conf.ParseIndex(manager, dir); err != nil {
84
85
			return err
		}
86
87
88
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
		if err != nil || !exists {
			return err
89
		}
90
91
92
		if manager.XMLVersion < 7 {
			return errors.New("Unsupported scheme manager description")
		}
93
94
95
96
97
98
99
		valid, err := conf.VerifySignature(manager.Identifier())
		if err != nil {
			return err
		}
		if !valid {
			return errors.New("Scheme manager signature was invalid")
		}
100
		conf.SchemeManagers[manager.Identifier()] = manager
101
		return conf.parseIssuerFolders(manager, dir)
102
103
104
105
	})
	if err != nil {
		return err
	}
106
	conf.initialized = true
107
108
109
	return nil
}

110
111
112
113
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

114
115
116
117
// PublicKey returns the specified public key, or nil if not present in the Configuration.
func (conf *Configuration) PublicKey(id IssuerIdentifier, counter int) (*gabi.PublicKey, error) {
	if _, contains := conf.publicKeys[id]; !contains {
		conf.publicKeys[id] = map[int]*gabi.PublicKey{}
118
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
119
			return nil, err
120
121
		}
	}
122
	return conf.publicKeys[id][counter], nil
123
124
}

125
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
126
	hash := sha256.Sum256([]byte(credid.String()))
127
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
128
129
}

130
131
132
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
133
134
135
136
	}
	return nil
}

137
// IsInitialized indicates whether this instance has successfully been initialized.
138
139
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
140
141
}

142
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
143
144
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
145
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
146
147
148
		if err != nil {
			return err
		}
149
150
		if !exists {
			return nil
151
		}
152
153
154
155
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
		conf.Issuers[issuer.Identifier()] = issuer
156
		return conf.parseCredentialsFolder(manager, dir+"/Issues/")
157
158
159
	})
}

160
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
161
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
162
	path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
163
164
165
166
167
168
169
170
171
172
173
	files, err := filepath.Glob(path)
	if err != nil {
		return err
	}

	for _, file := range files {
		filename := filepath.Base(file)
		count := filename[:len(filename)-4]
		i, err := strconv.Atoi(count)
		if err != nil {
			continue
174
		}
175
176
177
178
179
		bts, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.path, file))
		if err != nil {
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
180
181
182
		if err != nil {
			return err
		}
183
		pk.Issuer = issuerid.String()
184
		conf.publicKeys[issuerid][i] = pk
185
	}
186

187
188
189
	return nil
}

190
// parse $schememanager/$issuer/Issues/*/description.xml
191
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, path string) error {
192
193
	return iterateSubfolders(path, func(dir string) error {
		cred := &CredentialType{}
194
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
195
196
197
		if err != nil {
			return err
		}
198
199
200
201
202
		if !exists {
			return nil
		}
		if cred.XMLVersion < 4 {
			return errors.New("Unsupported credential type description")
203
		}
204
205
206
		credid := cred.Identifier()
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
		return nil
	})
}

// iterateSubfolders iterates over the subfolders of the specified path,
// calling the specified handler each time. If anything goes wrong, or
// if the caller returns a non-nil error, an error is immediately returned.
func iterateSubfolders(path string, handler func(string) error) error {
	dirs, err := filepath.Glob(path + "/*")
	if err != nil {
		return err
	}

	for _, dir := range dirs {
		stat, err := os.Stat(dir)
		if err != nil {
			return err
		}
		if !stat.IsDir() {
			continue
		}
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

237
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
238
239
240
241
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

242
	bts, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.path, path))
243
244
245
246
	if err != nil {
		return true, err
	}

247
	err = xml.Unmarshal(bts, description)
248
249
250
251
252
253
	if err != nil {
		return true, err
	}

	return true, nil
}
254

255
256
257
258
259
// Contains checks if the configuration contains the specified credential type.
func (conf *Configuration) Contains(cred CredentialTypeIdentifier) bool {
	return conf.SchemeManagers[cred.IssuerIdentifier().SchemeManagerIdentifier()] != nil &&
		conf.Issuers[cred.IssuerIdentifier()] != nil &&
		conf.CredentialTypes[cred] != nil
260
}
261

262
263
func (conf *Configuration) Copy(source string, parse bool) error {
	if err := fs.EnsureDirectoryExists(conf.path); err != nil {
264
265
266
		return err
	}

267
	err := filepath.Walk(source, filepath.WalkFunc(
268
269
270
271
272
273
		func(path string, info os.FileInfo, err error) error {
			if path == source {
				return nil
			}
			subpath := path[len(source):]
			if info.IsDir() {
274
				if err := fs.EnsureDirectoryExists(conf.path + subpath); err != nil {
275
276
277
278
279
280
281
282
					return err
				}
			} else {
				srcfile, err := os.Open(path)
				if err != nil {
					return err
				}
				defer srcfile.Close()
Sietse Ringers's avatar
Sietse Ringers committed
283
284
285
286
				bytes, err := ioutil.ReadAll(srcfile)
				if err != nil {
					return err
				}
287
				if err := fs.SaveFile(conf.path+subpath, bytes); err != nil {
288
289
290
291
292
293
					return err
				}
			}
			return nil
		}),
	)
294
295
296
297
298

	if err != nil {
		return err
	}
	if parse {
299
		return conf.ParseFolder()
300
301
	}
	return nil
302
}
303

304
305
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
306
func (conf *Configuration) DownloadSchemeManager(url string) (*SchemeManager, error) {
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
	if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
		url = "https://" + url
	}
	if url[len(url)-1] == '/' {
		url = url[:len(url)-1]
	}
	if strings.HasSuffix(url, "/description.xml") {
		url = url[:len(url)-len("/description.xml")]
	}
	b, err := NewHTTPTransport(url).GetBytes("/description.xml")
	if err != nil {
		return nil, err
	}
	manager := &SchemeManager{}
	if err = xml.Unmarshal(b, manager); err != nil {
		return nil, err
	}

	manager.URL = url // TODO?
	return manager, nil
}

329
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier) error {
330
	// Remove everything falling under the manager's responsibility
331
	for credid := range conf.CredentialTypes {
332
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
333
			delete(conf.CredentialTypes, credid)
334
335
		}
	}
336
	for issid := range conf.Issuers {
337
		if issid.SchemeManagerIdentifier() == id {
338
			delete(conf.Issuers, issid)
339
340
		}
	}
341
	for issid := range conf.publicKeys {
342
		if issid.SchemeManagerIdentifier() == id {
343
			delete(conf.publicKeys, issid)
344
345
		}
	}
346
	delete(conf.SchemeManagers, id)
347
	// Remove from storage
348
	return os.RemoveAll(fmt.Sprintf("%s/%s", conf.path, id.String()))
349
350
351
	// or, remove above iterations and call .ParseFolder()?
}

352
func (conf *Configuration) AddSchemeManager(manager *SchemeManager) error {
353
	name := manager.ID
354
	if err := fs.EnsureDirectoryExists(fmt.Sprintf("%s/%s", conf.path, name)); err != nil {
355
356
		return err
	}
357
358
359
360
361
362
363

	t := NewHTTPTransport(manager.URL)
	path := fmt.Sprintf("%s/%s", conf.path, name)
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
	if err := t.GetFile("/pk.pem", path+"/pk.pem"); err != nil {
364
365
		return err
	}
366
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
367
368
		return err
	}
369

370
	conf.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager
371
372
	return nil
}
373

374
375
376
377
378
379
380
381
382
383
384
385
386
func (conf *Configuration) DownloadSchemeManagerSignature(manager *SchemeManager) error {
	t := NewHTTPTransport(manager.URL)
	path := fmt.Sprintf("%s/%s", conf.path, manager.ID)

	if err := t.GetFile("/index", path+"/index"); err != nil {
		return err
	}
	if err := t.GetFile("/index.sig", path+"/index.sig"); err != nil {
		return err
	}
	return nil
}

387
func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, error) {
388
	var contains bool
389
390
391
392
393
394
	var err error
	downloaded := &IrmaIdentifierSet{
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
395
	updatedManagers := make(map[SchemeManagerIdentifier]struct{})
396

397
	for manid := range set.SchemeManagers {
398
		if _, contains = conf.SchemeManagers[manid]; !contains {
399
			return nil, errors.Errorf("Unknown scheme manager: %s", manid)
400
401
402
403
404
		}
	}

	transport := NewHTTPTransport("")
	for issid := range set.Issuers {
405
		if _, contains = conf.Issuers[issid]; !contains {
406
407
408
			manager := issid.SchemeManagerIdentifier()
			url := conf.SchemeManagers[manager].URL + "/" + issid.Name()
			path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), issid.Name())
409
410
411
			if err = transport.GetFile(url+"/description.xml", path+"/description.xml"); err != nil {
				return nil, err
			}
412
			if err = transport.GetFile(url+"/logo.png", path+"/logo.png"); err != nil {
413
414
				return nil, err
			}
415
			updatedManagers[manager] = struct{}{}
416
			downloaded.Issuers[issid] = struct{}{}
417
		}
Sietse Ringers's avatar
Sietse Ringers committed
418
419
420
	}
	for issid, list := range set.PublicKeys {
		for _, count := range list {
421
			pk, err := conf.PublicKey(issid, count)
Sietse Ringers's avatar
Sietse Ringers committed
422
423
424
425
426
427
			if err != nil {
				return nil, err
			}
			if pk == nil {
				manager := issid.SchemeManagerIdentifier()
				suffix := fmt.Sprintf("/%s/PublicKeys/%d.xml", issid.Name(), count)
428
				path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), suffix)
429
				if err = transport.GetFile(conf.SchemeManagers[manager].URL+suffix, path); err != nil {
430
					return nil, err
431
				}
432
				updatedManagers[manager] = struct{}{}
433
434
435
436
			}
		}
	}
	for credid := range set.CredentialTypes {
437
		if _, contains := conf.CredentialTypes[credid]; !contains {
438
439
			issuer := credid.IssuerIdentifier()
			manager := issuer.SchemeManagerIdentifier()
440
			local := fmt.Sprintf("%s/%s/%s/Issues", conf.path, manager.Name(), issuer.Name())
441
			if err := fs.EnsureDirectoryExists(local); err != nil {
442
				return nil, err
443
			}
444
445
			if err = transport.GetFile(
				fmt.Sprintf("%s/%s/Issues/%s/description.xml", conf.SchemeManagers[manager].URL, issuer.Name(), credid.Name()),
446
				fmt.Sprintf("%s/%s/description.xml", local, credid.Name()),
447
448
449
			); err != nil {
				return nil, err
			}
450
451
452
453
			_ = transport.GetFile( // Get logo but ignore errors, it is optional
				fmt.Sprintf("%s/%s/Issues/%s/logo.png", conf.SchemeManagers[manager].URL, issuer.Name(), credid.Name()),
				fmt.Sprintf("%s/%s/logo.png", local, credid.Name()),
			)
454
			updatedManagers[manager] = struct{}{}
455
			downloaded.CredentialTypes[credid] = struct{}{}
456
457
458
		}
	}

459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
	for manager := range updatedManagers {
		if err := conf.DownloadSchemeManagerSignature(conf.SchemeManagers[manager]); err != nil {
			return nil, err
		}
	}
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
	return downloaded, nil
}

func (i SchemeManagerIndex) String() string {
	var paths []string
	var b bytes.Buffer

	for path := range i {
		paths = append(paths, path)
	}
	sort.Strings(paths)

	for _, path := range paths {
		b.WriteString(hex.EncodeToString(i[path]))
		b.WriteString(" ")
		b.WriteString(path)
		b.WriteString("\n")
	}

	return b.String()
}

func (i SchemeManagerIndex) FromString(s string) error {
	for j, line := range strings.Split(s, "\n") {
		if len(line) == 0 {
			continue
		}
		parts := strings.Split(line, " ")
		if len(parts) != 2 {
			return errors.Errorf("Scheme manager index line %d has incorrect amount of parts", j)
		}
		hash, err := hex.DecodeString(parts[0])
		if err != nil {
			return err
		}
		i[parts[1]] = hash
	}

	return nil
}

func (conf *Configuration) ParseIndex(manager *SchemeManager, dir string) error {
	if err := fs.AssertPathExists(dir + "/index"); err != nil {
		return errors.New("Missing scheme manager index file")
	}
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
		return err
	}
	manager.index = make(map[string]ConfigurationFileHash)
	return manager.index.FromString(string(indexbts))
}

// ReadAuthenticatedFile reads the file at the specified path
// and verifies its authenticity by checking that the file hash
// is present in the (signed) scheme manager index file.
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, error) {
	signedHash, ok := manager.index[path]
	if !ok {
		return nil, errors.New("File not present in scheme manager index")
	}

	bts, err := ioutil.ReadFile(filepath.Join(conf.path, path))
	if err != nil {
		return nil, err
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
		return nil, errors.New("File hash invalid")
	}
	return bts, nil
}

// VerifySignature verifies the signature on the scheme manager index file
// (which contains the SHA256 hashes of all files under this scheme manager,
// which are used for verifying file authenticity).
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (bool, error) {
	dir := filepath.Join(conf.path, id.String())
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
		return false, errors.New("Missing scheme manager index file, signature, or public key")
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
		return false, err
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
		return false, err
	}
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
		return false, err
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
		return false, errors.New("Invalid scheme manager public key")
	}

	// Read and parse signature
	sig, err := ioutil.ReadFile(dir + "/index.sig")
	if err != nil {
		return false, err
	}
	ints := make([]*big.Int, 0, 2)
	_, err = asn1.Unmarshal(sig, &ints)

	// Verify signature
	return ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]), nil
582
}