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

import (
	"encoding/base64"
	"encoding/xml"
	"io/ioutil"
	"os"
	"path/filepath"
9
	"regexp"
10
11
	"strconv"

12
13
	"crypto/sha256"

14
15
16
17
	"fmt"

	"strings"

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

	"bytes"

	"encoding/hex"

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

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

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

Sietse Ringers's avatar
Sietse Ringers committed
42
43
44
	// Path to the irma_configuration folder that this instance represents
	Path string

45
46
47
	// DisabledSchemeManagers keeps track of scheme managers that did not parse  succesfully
	// (i.e., invalid signature, parsing error), and the problem that occurred when parsing them
	DisabledSchemeManagers map[SchemeManagerIdentifier]*SchemeManagerError
48

49
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
50
	reverseHashes map[string]CredentialTypeIdentifier
51
	initialized   bool
52
	assets        string
53
54
}

Sietse Ringers's avatar
Sietse Ringers committed
55
56
// ConfigurationFileHash encodes the SHA256 hash of an authenticated
// file under a scheme manager within the configuration folder.
57
58
type ConfigurationFileHash []byte

Sietse Ringers's avatar
Sietse Ringers committed
59
60
// SchemeManagerIndex is a (signed) list of files under a scheme manager
// along with their SHA266 hash
61
62
type SchemeManagerIndex map[string]ConfigurationFileHash

63
64
type SchemeManagerStatus string

65
66
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
67
	Status  SchemeManagerStatus
68
69
70
	Err     error
}

71
72
73
74
75
76
77
78
79
const (
	SchemeManagerStatusValid               = SchemeManagerStatus("Valid")
	SchemeManagerStatusUnprocessed         = SchemeManagerStatus("Unprocessed")
	SchemeManagerStatusInvalidIndex        = SchemeManagerStatus("InvalidIndex")
	SchemeManagerStatusInvalidSignature    = SchemeManagerStatus("InvalidSignature")
	SchemeManagerStatusParsingError        = SchemeManagerStatus("ParsingError")
	SchemeManagerStatusContentParsingError = SchemeManagerStatus("ContentParsingError")
)

80
81
82
83
func (sme SchemeManagerError) Error() string {
	return fmt.Sprintf("Error parsing scheme manager %s: %s", sme.Manager.Name(), sme.Err.Error())
}

84
// NewConfiguration returns a new configuration. After this
85
// ParseFolder() should be called to parse the specified path.
86
87
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
	conf = &Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
88
		Path:   path,
89
		assets: assets,
90
	}
91

92
93
94
95
	if conf.assets != "" { // If an assets folder is specified, then it must exist
		if err = fs.AssertPathExists(conf.assets); err != nil {
			return nil, errors.WrapPrefix(err, "Nonexistent assets folder specified", 0)
		}
96
	}
97
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
98
99
		return nil, err
	}
100

101
102
103
	// Init all maps
	conf.clear()

104
105
106
	return
}

107
func (conf *Configuration) clear() {
108
109
110
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
111
	conf.DisabledSchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManagerError)
112
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
113
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
114
115
116
117
118
119
120
}

// ParseFolder populates the current Configuration by parsing the storage path,
// listing the containing scheme managers, issuers and credential types.
func (conf *Configuration) ParseFolder() (err error) {
	// Init all maps
	conf.clear()
121

122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
	// Copy any new or updated scheme managers out of the assets into storage
	if conf.assets != "" {
		err = iterateSubfolders(conf.assets, func(dir string) error {
			scheme := NewSchemeManagerIdentifier(filepath.Base(dir))
			uptodate, err := conf.isUpToDate(scheme)
			if err != nil {
				return err
			}
			if !uptodate {
				_, err = conf.CopyManagerFromAssets(scheme)
			}
			return err
		})
		if err != nil {
			return err
		}
	}

	// Parse scheme managers in storage
141
	var mgrerr *SchemeManagerError
Sietse Ringers's avatar
Sietse Ringers committed
142
	err = iterateSubfolders(conf.Path, func(dir string) error {
143
144
		manager := NewSchemeManager(filepath.Base(dir))
		err := conf.ParseSchemeManagerFolder(dir, manager)
145
146
		if err == nil {
			return nil // OK, do next scheme manager folder
147
		}
148
149
150
151
		// If there is an error, and it is of type SchemeManagerError, return nil
		// so as to continue parsing other managers.
		var ok bool
		if mgrerr, ok = err.(*SchemeManagerError); ok {
152
			conf.DisabledSchemeManagers[manager.Identifier()] = mgrerr
153
			return nil
154
		}
155
		return err // Not a SchemeManagerError? return it & halt parsing now
156
157
	})
	if err != nil {
158
		return
159
	}
160
	conf.initialized = true
161
162
163
	if mgrerr != nil {
		return mgrerr
	}
164
	return
165
166
}

167
168
169
170
func (conf *Configuration) ParseOrRestoreFolder() error {
	err := conf.ParseFolder()
	var parse bool
	for id := range conf.DisabledSchemeManagers {
171
		parse, _ = conf.CopyManagerFromAssets(id)
172
173
174
175
176
177
178
	}
	if parse {
		return conf.ParseFolder()
	}
	return err
}

179
// ParseSchemeManagerFolder parses the entire tree of the specified scheme manager
180
// If err != nil then a problem occured
181
func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
182
183
184
185
186
187
188
	// From this point, keep it in our map even if it has an error. The user must check either:
	// - manager.Status == SchemeManagerStatusValid, aka "VALID"
	// - or equivalently, manager.Valid == true
	// before using any scheme manager for anything, and handle accordingly
	conf.SchemeManagers[manager.Identifier()] = manager

	// Ensure we return a SchemeManagerError when any error occurs
189
190
	defer func() {
		if err != nil {
191
192
193
194
195
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
196
197
198
		}
	}()

199
200
201
202
203
	err = fs.AssertPathExists(dir + "/description.xml")
	if err != nil {
		return
	}

204
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
205
206
		manager.Status = SchemeManagerStatusInvalidIndex
		return
207
	}
208

209
	err = conf.VerifySchemeManager(manager)
210
211
212
213
214
	if err != nil {
		manager.Status = SchemeManagerStatusInvalidSignature
		return
	}

215
216
217
218
219
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
220
221
222
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
223
224
	}

225
226
227
	ts, exists, err := readTimestamp(dir + "/timestamp")
	if err != nil || !exists {
		return errors.WrapPrefix(err, "Could not read scheme manager timestamp", 0)
228
	}
229
	manager.Timestamp = *ts
230

231
	if manager.XMLVersion < 7 {
232
		manager.Status = SchemeManagerStatusParsingError
233
		return errors.New("Unsupported scheme manager description")
234
	}
235

236
	err = conf.parseIssuerFolders(manager, dir)
237
238
239
240
241
242
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
243
244
245
	return
}

246
247
248
249
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

250
251
252
253
// 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{}
254
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
255
			return nil, err
256
257
		}
	}
258
	return conf.publicKeys[id][counter], nil
259
260
}

261
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
262
	hash := sha256.Sum256([]byte(credid.String()))
263
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
264
265
}

266
267
268
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
269
270
271
272
	}
	return nil
}

273
// IsInitialized indicates whether this instance has successfully been initialized.
274
275
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
276
277
}

278
279
280
281
282
283
284
285
286
// Prune removes any invalid scheme managers and everything they own from this Configuration
func (conf *Configuration) Prune() {
	for _, manager := range conf.SchemeManagers {
		if !manager.Valid {
			_ = conf.RemoveSchemeManager(manager.Identifier(), false) // does not return errors
		}
	}
}

287
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
288
289
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
290
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
291
292
293
		if err != nil {
			return err
		}
294
295
		if !exists {
			return nil
296
		}
297
298
299
300
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
		conf.Issuers[issuer.Identifier()] = issuer
301
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
302
		return conf.parseCredentialsFolder(manager, dir+"/Issues/")
303
304
305
	})
}

306
307
308
309
310
311
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

312
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
313
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
Sietse Ringers's avatar
Sietse Ringers committed
314
	path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
315
316
317
318
319
320
321
322
323
324
325
	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
326
		}
Sietse Ringers's avatar
Sietse Ringers committed
327
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
328
		if err != nil || !found {
329
330
331
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
332
333
334
		if err != nil {
			return err
		}
335
		pk.Issuer = issuerid.String()
336
		conf.publicKeys[issuerid][i] = pk
337
	}
338

339
340
341
	return nil
}

342
// parse $schememanager/$issuer/Issues/*/description.xml
343
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, path string) error {
344
345
	return iterateSubfolders(path, func(dir string) error {
		cred := &CredentialType{}
346
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
347
348
349
		if err != nil {
			return err
		}
350
351
352
353
354
		if !exists {
			return nil
		}
		if cred.XMLVersion < 4 {
			return errors.New("Unsupported credential type description")
355
		}
356
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
357
358
359
		credid := cred.Identifier()
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
		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
		}
381
382
383
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
384
385
386
387
388
389
390
391
392
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

393
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
394
395
396
397
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
398
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
399
400
401
	if !found {
		return false, nil
	}
402
403
404
405
	if err != nil {
		return true, err
	}

406
	err = xml.Unmarshal(bts, description)
407
408
409
410
411
412
	if err != nil {
		return true, err
	}

	return true, nil
}
413

414
415
416
417
418
// 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
419
}
420

421
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
422
423
424
	if conf.assets == "" {
		return true, nil
	}
425
426
427
428
	name := scheme.String()
	newTime, exists, err := readTimestamp(filepath.Join(conf.assets, name, "timestamp"))
	if err != nil || !exists {
		return true, errors.WrapPrefix(err, "Could not read asset timestamp of scheme "+name, 0)
429
	}
430
431
432
433
	// The storage version of the manager does not need to have a timestamp. If it does not, it is outdated.
	oldTime, exists, err := readTimestamp(filepath.Join(conf.Path, name, "timestamp"))
	if err != nil {
		return true, err
434
	}
435
	return exists && !newTime.After(*oldTime), nil
436
437
}

438
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
439
	if conf.assets == "" {
440
		return false, nil
441
	}
442
443
444
445
446
	// Remove old version; we want an exact copy of the assets version
	// not a merge of the assets version and the storage version
	name := scheme.String()
	if err := os.RemoveAll(filepath.Join(conf.Path, name)); err != nil {
		return false, err
447
	}
448
449
450
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
451
452
453
	)
}

454
455
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
456
func DownloadSchemeManager(url string) (*SchemeManager, error) {
457
458
459
460
461
462
463
464
465
	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")]
	}
466
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
467
468
469
	if err != nil {
		return nil, err
	}
470
	manager := NewSchemeManager("")
471
472
473
474
475
476
477
478
	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
479
480
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
481
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
482
	// Remove everything falling under the manager's responsibility
483
	for credid := range conf.CredentialTypes {
484
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
485
			delete(conf.CredentialTypes, credid)
486
487
		}
	}
488
	for issid := range conf.Issuers {
489
		if issid.SchemeManagerIdentifier() == id {
490
			delete(conf.Issuers, issid)
491
492
		}
	}
493
	for issid := range conf.publicKeys {
494
		if issid.SchemeManagerIdentifier() == id {
495
			delete(conf.publicKeys, issid)
496
497
		}
	}
498
	delete(conf.SchemeManagers, id)
499
500

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
501
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
502
503
	}
	return nil
504
505
}

506
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
507
// provided its signature is valid.
508
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
509
	name := manager.ID
510
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
511
512
		return err
	}
513
514

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
515
	path := fmt.Sprintf("%s/%s", conf.Path, name)
516
517
518
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
519
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
520
521
		return err
	}
522
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
523
524
		return err
	}
525
526
527
528
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
529

530
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
531
532
533
534
535
536
}

// 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)
Sietse Ringers's avatar
Sietse Ringers committed
537
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
538
539
540
541
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
542
		return
543
544
	}
	if err = t.GetFile("index.sig", sig); err != nil {
545
		return
546
	}
Sietse Ringers's avatar
Sietse Ringers committed
547
548
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
549
		return
Sietse Ringers's avatar
Sietse Ringers committed
550
551
	}
	if !valid {
552
		err = errors.New("Scheme manager signature invalid")
Sietse Ringers's avatar
Sietse Ringers committed
553
	}
554
	return
555
}
556

Sietse Ringers's avatar
Sietse Ringers committed
557
558
559
// 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.
560
561
func (conf *Configuration) Download(session IrmaSession) (downloaded *IrmaIdentifierSet, err error) {
	managers := make(map[string]struct{}) // Managers that we must update
562
	downloaded = &IrmaIdentifierSet{
563
564
565
566
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
567

568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
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
	// Calculate which scheme managers must be updated
	if err = conf.checkIssuers(session.Identifiers(), managers); err != nil {
		return
	}
	if err = conf.checkCredentialTypes(session, managers); err != nil {
		return
	}

	// Update the scheme managers found above and parse them, if necessary
	for id := range managers {
		if err = conf.UpdateSchemeManager(NewSchemeManagerIdentifier(id), downloaded); err != nil {
			return
		}
	}
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
	return
}

func (conf *Configuration) checkCredentialTypes(session IrmaSession, managers map[string]struct{}) error {
	var disjunctions AttributeDisjunctionList
	var typ *CredentialType
	var contains bool

	switch s := session.(type) {
	case *IssuanceRequest:
		for _, credreq := range s.Credentials {
			// First check if we have this credential type
			typ, contains = conf.CredentialTypes[*credreq.CredentialTypeID]
			if !contains {
				managers[credreq.CredentialTypeID.Root()] = struct{}{}
				continue
			}
			newAttrs := make(map[string]string)
			for k, v := range credreq.Attributes {
				newAttrs[k] = v
			}
			// For each of the attributes in the credentialtype, see if it is present; if so remove it from newAttrs
			// If not, check that it is optional; if not the credentialtype must be updated
			for _, attrtyp := range typ.Attributes {
				_, contains = newAttrs[attrtyp.ID]
				if !contains && !attrtyp.IsOptional() {
					managers[credreq.CredentialTypeID.Root()] = struct{}{}
					break
				}
				delete(newAttrs, attrtyp.ID)
			}
			// If there is anything left in newAttrs, then these are attributes that are not in the credentialtype
			if len(newAttrs) > 0 {
				managers[credreq.CredentialTypeID.Root()] = struct{}{}
			}
		}
		disjunctions = s.Disclose
	case *DisclosureRequest:
		disjunctions = s.Content
	case *SignatureRequest:
		disjunctions = s.Content
	}

	for _, disjunction := range disjunctions {
		for _, attrid := range disjunction.Attributes {
			credid := attrid.CredentialTypeIdentifier()
			if typ, contains = conf.CredentialTypes[credid]; !contains {
				managers[credid.Root()] = struct{}{}
			}
			if !typ.ContainsAttribute(attrid) {
				managers[credid.Root()] = struct{}{}
			}
		}
	}

	return nil
}

func (conf *Configuration) checkIssuers(set *IrmaIdentifierSet, managers map[string]struct{}) error {
644
	for issid := range set.Issuers {
645
		if _, contains := conf.Issuers[issid]; !contains {
646
			managers[issid.Root()] = struct{}{}
647
		}
Sietse Ringers's avatar
Sietse Ringers committed
648
	}
649
650
651
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
652
			if err != nil {
653
				return err
Sietse Ringers's avatar
Sietse Ringers committed
654
655
			}
			if pk == nil {
656
				managers[issid.Root()] = struct{}{}
657
658
659
			}
		}
	}
660
	return nil
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
}

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
682
// FromString populates this index by parsing the specified string.
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
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
}

702
// parseIndex parses the index file of the specified manager.
703
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
704
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
705
	if err := fs.AssertPathExists(path); err != nil {
706
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
707
	}
Sietse Ringers's avatar
Sietse Ringers committed
708
	indexbts, err := ioutil.ReadFile(path)
709
	if err != nil {
710
		return nil, err
711
	}
712
713
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
714
715
}

716
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
717
718
719
720
721
722
723
724
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
		return err
	}
	if !valid {
		return errors.New("Scheme manager signature was invalid")
	}

725
	for file := range manager.index {
Sietse Ringers's avatar
Sietse Ringers committed
726
		exists, err := fs.PathExists(filepath.Join(conf.Path, file))
727
728
729
730
731
732
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
733
		// Don't care about the actual bytes
734
		if _, _, err := conf.ReadAuthenticatedFile(manager, file); err != nil {
735
736
737
738
739
740
741
			return err
		}
	}

	return nil
}

742
743
744
// 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.
745
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
746
	signedHash, ok := manager.index[path]
747
	if !ok {
748
		return nil, false, nil
749
750
	}

Sietse Ringers's avatar
Sietse Ringers committed
751
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
752
	if err != nil {
753
		return nil, true, err
754
755
756
757
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
758
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
759
	}
760
	return bts, true, nil
761
762
763
764
765
}

// 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).
766
767
768
769
770
771
772
773
774
775
776
777
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")
			}
		}
	}()

Sietse Ringers's avatar
Sietse Ringers committed
778
	dir := filepath.Join(conf.Path, id.String())
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
	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
815
}
816
817
818
819

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835

func (hash ConfigurationFileHash) Equal(other ConfigurationFileHash) bool {
	return bytes.Equal(hash, other)
}

// UpdateSchemeManager syncs the stored version within the irma_configuration directory
// with the remote version at the scheme manager's URL, downloading and storing
// new and modified files, according to the index files of both versions.
// It stores the identifiers of new or updated credential types or issuers in the second parameter.
// Note: any newly downloaded files are not yet parsed and inserted into conf.
func (conf *Configuration) UpdateSchemeManager(id SchemeManagerIdentifier, downloaded *IrmaIdentifierSet) (err error) {
	manager, contains := conf.SchemeManagers[id]
	if !contains {
		return errors.Errorf("Cannot update unknown scheme manager %s", id)
	}

836
837
838
839
840
841
842
843
844
845
	// Check remote timestamp and see if we have to do anything
	transport := NewHTTPTransport(manager.URL + "/")
	timestampBts, err := transport.GetBytes("timestamp")
	if err != nil {
		return err
	}
	timestamp, err := parseTimestamp(timestampBts)
	if err != nil {
		return err
	}
846
	if !manager.Timestamp.Before(*timestamp) {
847
848
849
		return nil
	}

850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
	// Download the new index and its signature, and check that the new index
	// is validly signed by the new signature
	// By aborting immediately in case of error, and restoring backup versions
	// of the index and signature, we leave our stored copy of the scheme manager
	// intact.
	if err = conf.DownloadSchemeManagerSignature(manager); err != nil {
		return
	}
	newIndex, err := conf.parseIndex(manager.ID, manager)
	if err != nil {
		return
	}

	issPattern := regexp.MustCompile("(.+)/(.+)/description\\.xml")
	credPattern := regexp.MustCompile("(.+)/(.+)/Issues/(.+)/description\\.xml")

	// TODO: how to recover/fix local copy if err != nil below?
	for filename, newHash := range newIndex {
868
		path := filepath.Join(conf.Path, filename)
869
		oldHash, known := manager.index[filename]
870
871
872
873
874
875
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
876
877
878
879
880
881
882
883
			continue // nothing to do, we already have this file
		}
		// Ensure that the folder in which to write the file exists
		if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
			return err
		}
		stripped := filename[len(manager.ID)+1:] // Scheme manager URL already ends with its name
		// Download the new file, store it in our own irma_configuration folder
884
		if err = transport.GetSignedFile(stripped, path, newHash); err != nil {
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
			return
		}
		// See if the file is a credential type or issuer, and add it to the downloaded set if so
		if downloaded == nil {
			continue
		}
		var matches []string
		matches = issPattern.FindStringSubmatch(filename)
		if len(matches) == 3 {
			issid := NewIssuerIdentifier(fmt.Sprintf("%s.%s", matches[1], matches[2]))
			downloaded.Issuers[issid] = struct{}{}
		}
		matches = credPattern.FindStringSubmatch(filename)
		if len(matches) == 4 {
			credid := NewCredentialTypeIdentifier(fmt.Sprintf("%s.%s.%s", matches[1], matches[2], matches[3]))
			downloaded.CredentialTypes[credid] = struct{}{}
		}
	}

	manager.index = newIndex
	return
}