irmaconfig.go 18.6 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
}

Sietse Ringers's avatar
Sietse Ringers committed
47
48
// ConfigurationFileHash encodes the SHA256 hash of an authenticated
// file under a scheme manager within the configuration folder.
49
50
type ConfigurationFileHash []byte

Sietse Ringers's avatar
Sietse Ringers committed
51
52
// SchemeManagerIndex is a (signed) list of files under a scheme manager
// along with their SHA266 hash
53
54
type SchemeManagerIndex map[string]ConfigurationFileHash

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

62
	if err = fs.EnsureDirectoryExists(conf.path); err != nil {
63
64
		return nil, err
	}
65
	if assets != "" {
66
		if err = conf.Copy(assets, false); err != nil {
67
68
69
70
			return nil, err
		}
	}

71
72
73
	return
}

74
// ParseFolder populates the current Configuration by parsing the storage path,
75
// listing the containing scheme managers, issuers and credential types.
76
func (conf *Configuration) ParseFolder() error {
77
	// Init all maps
78
79
80
81
	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)
82

83
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
84

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

114
115
116
117
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

118
119
120
121
// 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{}
122
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
123
			return nil, err
124
125
		}
	}
126
	return conf.publicKeys[id][counter], nil
127
128
}

129
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
130
	hash := sha256.Sum256([]byte(credid.String()))
131
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
132
133
}

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

141
// IsInitialized indicates whether this instance has successfully been initialized.
142
143
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
144
145
}

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

164
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
165
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
166
	path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
167
168
169
170
171
172
173
174
175
176
177
	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
178
		}
179
180
181
182
183
		bts, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.path, file))
		if err != nil {
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
184
185
186
		if err != nil {
			return err
		}
187
		pk.Issuer = issuerid.String()
188
		conf.publicKeys[issuerid][i] = pk
189
	}
190

191
192
193
	return nil
}

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

	return nil
}

244
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
245
246
247
248
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

249
	bts, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.path, path))
250
251
252
253
	if err != nil {
		return true, err
	}

254
	err = xml.Unmarshal(bts, description)
255
256
257
258
259
260
	if err != nil {
		return true, err
	}

	return true, nil
}
261

262
263
264
265
266
// 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
267
}
268

Sietse Ringers's avatar
Sietse Ringers committed
269
270
// Copy recursively copies the directory tree at source into the directory
// of this Configuration.
271
272
func (conf *Configuration) Copy(source string, parse bool) error {
	if err := fs.EnsureDirectoryExists(conf.path); err != nil {
273
274
275
		return err
	}

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

	if err != nil {
		return err
	}
	if parse {
308
		return conf.ParseFolder()
309
310
	}
	return nil
311
}
312

313
314
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
315
func (conf *Configuration) DownloadSchemeManager(url string) (*SchemeManager, error) {
316
317
318
319
320
321
322
323
324
	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")]
	}
325
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
326
327
328
329
330
331
332
333
334
335
336
337
	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
}

Sietse Ringers's avatar
Sietse Ringers committed
338
339
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
340
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier) error {
341
	// Remove everything falling under the manager's responsibility
342
	for credid := range conf.CredentialTypes {
343
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
344
			delete(conf.CredentialTypes, credid)
345
346
		}
	}
347
	for issid := range conf.Issuers {
348
		if issid.SchemeManagerIdentifier() == id {
349
			delete(conf.Issuers, issid)
350
351
		}
	}
352
	for issid := range conf.publicKeys {
353
		if issid.SchemeManagerIdentifier() == id {
354
			delete(conf.publicKeys, issid)
355
356
		}
	}
357
	delete(conf.SchemeManagers, id)
358
	// Remove from storage
359
	return os.RemoveAll(fmt.Sprintf("%s/%s", conf.path, id.String()))
360
361
362
	// or, remove above iterations and call .ParseFolder()?
}

Sietse Ringers's avatar
Sietse Ringers committed
363
364
// AddSchemeManager adds the specified scheme manager to this Configuration,
// provided its signature is valid.
365
func (conf *Configuration) AddSchemeManager(manager *SchemeManager) error {
366
	name := manager.ID
367
	if err := fs.EnsureDirectoryExists(fmt.Sprintf("%s/%s", conf.path, name)); err != nil {
368
369
		return err
	}
370
371
372
373
374
375

	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
	}
376
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
377
378
		return err
	}
379
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
380
381
		return err
	}
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405

	conf.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager
	return nil
}

// DownloadSchemeManagerSignature downloads, stores and verifies the latest version
// of the index file and signature of the specified manager.
func (conf *Configuration) DownloadSchemeManagerSignature(manager *SchemeManager) (err error) {
	t := NewHTTPTransport(manager.URL)
	path := fmt.Sprintf("%s/%s", conf.path, manager.ID)
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	// Backup so we can restore last valid signature if the new signature is invalid
	if err := conf.backupManagerSignature(index, sig); err != nil {
		return err
	}

	if err = t.GetFile("index", index); err != nil {
		return err
	}
	if err = t.GetFile("index.sig", sig); err != nil {
		return err
	}
Sietse Ringers's avatar
Sietse Ringers committed
406
407
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
408
		_ = conf.restoreManagerSignature(index, sig)
Sietse Ringers's avatar
Sietse Ringers committed
409
410
411
		return err
	}
	if !valid {
412
		_ = conf.restoreManagerSignature(index, sig)
Sietse Ringers's avatar
Sietse Ringers committed
413
414
		return errors.New("Scheme manager signature invalid")
	}
415

416
417
	return nil
}
418

419
420
421
422
423
424
425
426
427
func (conf *Configuration) backupManagerSignature(index, sig string) error {
	if err := fs.Copy(index, index+".backup"); err != nil {
		return err
	}
	if err := fs.Copy(sig, sig+".backup"); err != nil {
		return err
	}
	return nil
}
428

429
430
func (conf *Configuration) restoreManagerSignature(index, sig string) error {
	if err := fs.Copy(index+".backup", index); err != nil {
431
432
		return err
	}
433
	if err := fs.Copy(sig+".backup", sig); err != nil {
434
435
436
437
438
		return err
	}
	return nil
}

Sietse Ringers's avatar
Sietse Ringers committed
439
440
441
// Download downloads the issuers, credential types and public keys specified in set
// if the current Configuration does not already have them,  and checks their authenticity
// using the scheme manager index.
442
func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, error) {
443
	var contains bool
444
445
446
447
448
449
	var err error
	downloaded := &IrmaIdentifierSet{
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
450
	updatedManagers := make(map[SchemeManagerIdentifier]struct{})
451

452
	for manid := range set.SchemeManagers {
453
		if _, contains = conf.SchemeManagers[manid]; !contains {
454
			return nil, errors.Errorf("Unknown scheme manager: %s", manid)
455
456
457
458
459
		}
	}

	transport := NewHTTPTransport("")
	for issid := range set.Issuers {
460
		if _, contains = conf.Issuers[issid]; !contains {
461
462
463
			manager := issid.SchemeManagerIdentifier()
			url := conf.SchemeManagers[manager].URL + "/" + issid.Name()
			path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), issid.Name())
464
465
466
			if err = transport.GetFile(url+"/description.xml", path+"/description.xml"); err != nil {
				return nil, err
			}
467
			if err = transport.GetFile(url+"/logo.png", path+"/logo.png"); err != nil {
468
469
				return nil, err
			}
470
			updatedManagers[manager] = struct{}{}
471
			downloaded.Issuers[issid] = struct{}{}
472
		}
Sietse Ringers's avatar
Sietse Ringers committed
473
474
475
	}
	for issid, list := range set.PublicKeys {
		for _, count := range list {
476
			pk, err := conf.PublicKey(issid, count)
Sietse Ringers's avatar
Sietse Ringers committed
477
478
479
480
481
482
			if err != nil {
				return nil, err
			}
			if pk == nil {
				manager := issid.SchemeManagerIdentifier()
				suffix := fmt.Sprintf("/%s/PublicKeys/%d.xml", issid.Name(), count)
483
				path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), suffix)
484
				if err = transport.GetFile(conf.SchemeManagers[manager].URL+suffix, path); err != nil {
485
					return nil, err
486
				}
487
				updatedManagers[manager] = struct{}{}
488
489
490
491
			}
		}
	}
	for credid := range set.CredentialTypes {
492
		if _, contains := conf.CredentialTypes[credid]; !contains {
493
494
			issuer := credid.IssuerIdentifier()
			manager := issuer.SchemeManagerIdentifier()
495
			local := fmt.Sprintf("%s/%s/%s/Issues", conf.path, manager.Name(), issuer.Name())
496
			if err := fs.EnsureDirectoryExists(local); err != nil {
497
				return nil, err
498
			}
499
500
			if err = transport.GetFile(
				fmt.Sprintf("%s/%s/Issues/%s/description.xml", conf.SchemeManagers[manager].URL, issuer.Name(), credid.Name()),
501
				fmt.Sprintf("%s/%s/description.xml", local, credid.Name()),
502
503
504
			); err != nil {
				return nil, err
			}
505
506
507
508
			_ = 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()),
			)
509
			updatedManagers[manager] = struct{}{}
510
			downloaded.CredentialTypes[credid] = struct{}{}
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
	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()
}

Sietse Ringers's avatar
Sietse Ringers committed
544
// FromString populates this index by parsing the specified string.
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
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
}

Sietse Ringers's avatar
Sietse Ringers committed
564
565
566
567
// ParseIndex parses the index file of the specified manager.
func (conf *Configuration) ParseIndex(manager *SchemeManager) error {
	path := filepath.Join(conf.path, manager.ID, "index")
	if err := fs.AssertPathExists(path); err != nil {
568
569
		return errors.New("Missing scheme manager index file")
	}
Sietse Ringers's avatar
Sietse Ringers committed
570
	indexbts, err := ioutil.ReadFile(path)
571
572
573
	if err != nil {
		return err
	}
574
575
	manager.Index = make(map[string]ConfigurationFileHash)
	return manager.Index.FromString(string(indexbts))
576
577
578
579
580
581
}

// 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) {
582
	signedHash, ok := manager.Index[path]
583
584
585
586
587
588
589
590
591
592
593
	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) {
594
		return nil, errors.Errorf("Hash of %s does not match scheme manager index", path)
595
596
597
598
599
600
601
	}
	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).
602
603
604
605
606
607
608
609
610
611
612
613
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (valid bool, err error) {
	defer func() {
		if r := recover(); r != nil {
			valid = false
			if e, ok := r.(error); ok {
				err = errors.Errorf("Scheme manager index signature failed to verify: %s", e.Error())
			} else {
				err = errors.New("Scheme manager index signature failed to verify")
			}
		}
	}()

614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
	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
651
}
652
653
654
655

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}