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
	initialized   bool
44
45
	path          string
	assets        string
46
47
}

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

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

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

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

73
74
75
	return
}

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

85
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
86

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

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

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

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

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

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

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

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

193
194
195
	return nil
}

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

	return nil
}

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

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

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

	return true, nil
}
263

264
265
266
267
268
// 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
269
}
270

271
272
273
// CopyFromAssets recursively copies the directory tree from the assets folder
// into the directory of this Configuration.
func (conf *Configuration) CopyFromAssets(parse bool) error {
274
	if err := fs.EnsureDirectoryExists(conf.path); err != nil {
275
276
277
		return err
	}

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

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

315
316
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
317
func (conf *Configuration) DownloadSchemeManager(url string) (*SchemeManager, error) {
318
319
320
321
322
323
324
325
326
	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")]
	}
327
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
328
329
330
331
332
333
334
335
336
337
338
339
	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
340
341
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
342
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier) error {
343
	// Remove everything falling under the manager's responsibility
344
	for credid := range conf.CredentialTypes {
345
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
346
			delete(conf.CredentialTypes, credid)
347
348
		}
	}
349
	for issid := range conf.Issuers {
350
		if issid.SchemeManagerIdentifier() == id {
351
			delete(conf.Issuers, issid)
352
353
		}
	}
354
	for issid := range conf.publicKeys {
355
		if issid.SchemeManagerIdentifier() == id {
356
			delete(conf.publicKeys, issid)
357
358
		}
	}
359
	delete(conf.SchemeManagers, id)
360
	// Remove from storage
361
	return os.RemoveAll(fmt.Sprintf("%s/%s", conf.path, id.String()))
362
363
364
	// or, remove above iterations and call .ParseFolder()?
}

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

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

	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
408
409
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
410
		_ = conf.restoreManagerSignature(index, sig)
Sietse Ringers's avatar
Sietse Ringers committed
411
412
413
		return err
	}
	if !valid {
414
		_ = conf.restoreManagerSignature(index, sig)
Sietse Ringers's avatar
Sietse Ringers committed
415
416
		return errors.New("Scheme manager signature invalid")
	}
417

418
419
	return nil
}
420

421
422
423
424
425
426
427
428
429
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
}
430

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

Sietse Ringers's avatar
Sietse Ringers committed
441
442
443
// 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.
444
func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, error) {
445
	var contains bool
446
447
448
449
450
451
	var err error
	downloaded := &IrmaIdentifierSet{
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
452
	updatedManagers := make(map[SchemeManagerIdentifier]struct{})
453

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

	transport := NewHTTPTransport("")
	for issid := range set.Issuers {
462
		if _, contains = conf.Issuers[issid]; !contains {
463
464
465
			manager := issid.SchemeManagerIdentifier()
			url := conf.SchemeManagers[manager].URL + "/" + issid.Name()
			path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), issid.Name())
466
467
468
			if err = transport.GetFile(url+"/description.xml", path+"/description.xml"); err != nil {
				return nil, err
			}
469
			if err = transport.GetFile(url+"/logo.png", path+"/logo.png"); err != nil {
470
471
				return nil, err
			}
472
			updatedManagers[manager] = struct{}{}
473
			downloaded.Issuers[issid] = struct{}{}
474
		}
Sietse Ringers's avatar
Sietse Ringers committed
475
476
477
	}
	for issid, list := range set.PublicKeys {
		for _, count := range list {
478
			pk, err := conf.PublicKey(issid, count)
Sietse Ringers's avatar
Sietse Ringers committed
479
480
481
482
483
484
			if err != nil {
				return nil, err
			}
			if pk == nil {
				manager := issid.SchemeManagerIdentifier()
				suffix := fmt.Sprintf("/%s/PublicKeys/%d.xml", issid.Name(), count)
485
				path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), suffix)
486
				if err = transport.GetFile(conf.SchemeManagers[manager].URL+suffix, path); err != nil {
487
					return nil, err
488
				}
489
				updatedManagers[manager] = struct{}{}
490
491
492
493
			}
		}
	}
	for credid := range set.CredentialTypes {
494
		if _, contains := conf.CredentialTypes[credid]; !contains {
495
496
			issuer := credid.IssuerIdentifier()
			manager := issuer.SchemeManagerIdentifier()
497
			local := fmt.Sprintf("%s/%s/%s/Issues", conf.path, manager.Name(), issuer.Name())
498
			if err := fs.EnsureDirectoryExists(local); err != nil {
499
				return nil, err
500
			}
501
502
			if err = transport.GetFile(
				fmt.Sprintf("%s/%s/Issues/%s/description.xml", conf.SchemeManagers[manager].URL, issuer.Name(), credid.Name()),
503
				fmt.Sprintf("%s/%s/description.xml", local, credid.Name()),
504
505
506
			); err != nil {
				return nil, err
			}
507
508
509
510
			_ = 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()),
			)
511
			updatedManagers[manager] = struct{}{}
512
			downloaded.CredentialTypes[credid] = struct{}{}
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
	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
546
// FromString populates this index by parsing the specified string.
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
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
566
567
568
569
// 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 {
570
571
		return errors.New("Missing scheme manager index file")
	}
Sietse Ringers's avatar
Sietse Ringers committed
572
	indexbts, err := ioutil.ReadFile(path)
573
574
575
	if err != nil {
		return err
	}
576
577
	manager.Index = make(map[string]ConfigurationFileHash)
	return manager.Index.FromString(string(indexbts))
578
579
580
581
582
583
}

// 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) {
584
	signedHash, ok := manager.Index[path]
585
586
587
588
589
590
591
592
593
594
595
	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) {
596
		return nil, errors.Errorf("Hash of %s does not match scheme manager index", path)
597
598
599
600
601
602
603
	}
	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).
604
605
606
607
608
609
610
611
612
613
614
615
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")
			}
		}
	}()

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
651
652
	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
653
}
654
655
656
657

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