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

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

14
15
	"crypto/sha256"

16
17
18
19
	"fmt"

	"strings"

20
21
22
23
24
25
26
27
28
29
30
31
	"sort"

	"bytes"

	"encoding/hex"

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

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

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

Sietse Ringers's avatar
Sietse Ringers committed
45
46
47
	// Path to the irma_configuration folder that this instance represents
	Path string

48
49
50
	// 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
51

52
53
	Warnings []string

54
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
55
	reverseHashes map[string]CredentialTypeIdentifier
56
	initialized   bool
57
	assets        string
58
59
}

Sietse Ringers's avatar
Sietse Ringers committed
60
61
// ConfigurationFileHash encodes the SHA256 hash of an authenticated
// file under a scheme manager within the configuration folder.
62
63
type ConfigurationFileHash []byte

Sietse Ringers's avatar
Sietse Ringers committed
64
65
// SchemeManagerIndex is a (signed) list of files under a scheme manager
// along with their SHA266 hash
66
67
type SchemeManagerIndex map[string]ConfigurationFileHash

68
69
type SchemeManagerStatus string

70
71
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
72
	Status  SchemeManagerStatus
73
74
75
	Err     error
}

76
77
78
79
80
81
82
const (
	SchemeManagerStatusValid               = SchemeManagerStatus("Valid")
	SchemeManagerStatusUnprocessed         = SchemeManagerStatus("Unprocessed")
	SchemeManagerStatusInvalidIndex        = SchemeManagerStatus("InvalidIndex")
	SchemeManagerStatusInvalidSignature    = SchemeManagerStatus("InvalidSignature")
	SchemeManagerStatusParsingError        = SchemeManagerStatus("ParsingError")
	SchemeManagerStatusContentParsingError = SchemeManagerStatus("ContentParsingError")
83

84
85
	pubkeyPattern  = "%s/%s/%s/PublicKeys/*.xml"
	privkeyPattern = "%s/%s/%s/PrivateKeys/*.xml"
86
87
)

88
89
90
91
func (sme SchemeManagerError) Error() string {
	return fmt.Sprintf("Error parsing scheme manager %s: %s", sme.Manager.Name(), sme.Err.Error())
}

92
// NewConfiguration returns a new configuration. After this
93
// ParseFolder() should be called to parse the specified path.
94
95
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
	conf = &Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
96
		Path:   path,
97
		assets: assets,
98
	}
99

100
101
102
103
	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)
		}
104
	}
105
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
106
107
		return nil, err
	}
108

109
110
111
	// Init all maps
	conf.clear()

112
113
114
	return
}

115
func (conf *Configuration) clear() {
116
117
118
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
119
	conf.Attributes = make(map[AttributeTypeIdentifier]*AttributeType)
120
	conf.DisabledSchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManagerError)
121
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
122
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
123
124
125
126
127
128
129
}

// 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()
130

131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
	// 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
150
	var mgrerr *SchemeManagerError
Sietse Ringers's avatar
Sietse Ringers committed
151
	err = iterateSubfolders(conf.Path, func(dir string) error {
152
153
		manager := NewSchemeManager(filepath.Base(dir))
		err := conf.ParseSchemeManagerFolder(dir, manager)
154
155
		if err == nil {
			return nil // OK, do next scheme manager folder
156
		}
157
158
159
160
		// 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 {
161
			conf.DisabledSchemeManagers[manager.Identifier()] = mgrerr
162
			return nil
163
		}
164
		return err // Not a SchemeManagerError? return it & halt parsing now
165
166
	})
	if err != nil {
167
		return
168
	}
169
	conf.initialized = true
170
171
172
	if mgrerr != nil {
		return mgrerr
	}
173
	return
174
175
}

176
177
178
179
180
181
182
// ParseOrRestoreFolder parses the irma_configuration folder, and when possible attempts to restore
// any broken scheme managers from their remote.
// Any error encountered during parsing is considered recoverable only if it is of type *SchemeManagerError;
// In this case the scheme in which it occured is downloaded from its remote and re-parsed.
// If any other error is encountered at any time, it is returned immediately.
// If no error is returned, parsing and possibly restoring has been succesfull, and there should be no
// disabled scheme managers.
183
184
func (conf *Configuration) ParseOrRestoreFolder() error {
	err := conf.ParseFolder()
185
186
187
	// Only in case of a *SchemeManagerError might we be able to recover
	if _, isSchemeMgrErr := err.(*SchemeManagerError); !isSchemeMgrErr {
		return err
188
	}
189
190

	for id := range conf.DisabledSchemeManagers {
191
192
193
194
195
196
197
198
199
		if err = conf.ReinstallSchemeManager(conf.SchemeManagers[id]); err == nil {
			continue
		}
		if _, err = conf.CopyManagerFromAssets(id); err != nil {
			return err // File system error, too serious, bail out now
		}
		name := id.String()
		if err = conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), NewSchemeManager(name)); err == nil {
			delete(conf.DisabledSchemeManagers, id)
200
		}
201
	}
202

203
204
205
	return err
}

206
// ParseSchemeManagerFolder parses the entire tree of the specified scheme manager
207
// If err != nil then a problem occured
208
func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
209
210
211
212
213
214
215
	// 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
216
217
	defer func() {
		if err != nil {
218
219
220
221
222
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
223
224
225
		}
	}()

226
227
	// Verify signature and read scheme manager description
	if err = conf.VerifySignature(manager.Identifier()); err != nil {
228
229
		return
	}
230
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
231
232
		manager.Status = SchemeManagerStatusInvalidIndex
		return
233
	}
234
235
236
237
238
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
239
240
241
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
242
	}
243
244
	if err = conf.checkScheme(manager, dir); err != nil {
		return
245
	}
246
247
248
249
250
251
252

	// Verify that all other files are validly signed
	err = conf.VerifySchemeManager(manager)
	if err != nil {
		manager.Status = SchemeManagerStatusInvalidSignature
		return
	}
253

254
	// Read timestamp indicating time of last modification
255
256
257
	ts, exists, err := readTimestamp(dir + "/timestamp")
	if err != nil || !exists {
		return errors.WrapPrefix(err, "Could not read scheme manager timestamp", 0)
258
	}
259
	manager.Timestamp = *ts
260

261
	// Parse contained issuers and credential types
262
	err = conf.parseIssuerFolders(manager, dir)
263
264
265
266
267
268
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
269
270
271
	return
}

272
273
274
275
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

276
277
278
// 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 {
279
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
280
			return nil, err
281
282
		}
	}
283
	return conf.publicKeys[id][counter], nil
284
285
}

286
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
287
	hash := sha256.Sum256([]byte(credid.String()))
288
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
289
290
}

291
292
293
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
294
295
296
297
	}
	return nil
}

298
// IsInitialized indicates whether this instance has successfully been initialized.
299
300
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
301
302
}

303
304
305
306
307
308
309
310
311
// 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
		}
	}
}

312
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
313
314
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
315
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
316
317
318
		if err != nil {
			return err
		}
319
320
		if !exists {
			return nil
321
		}
322
323
324
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
325

326
		if err = conf.checkIssuer(manager, issuer, dir); err != nil {
327
328
329
			return err
		}

330
		conf.Issuers[issuer.Identifier()] = issuer
331
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
332
		return conf.parseCredentialsFolder(manager, issuer, dir+"/Issues/")
333
334
335
	})
}

336
337
338
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
	name := id.String()
	for iss := range conf.Issuers {
		if iss.Root() == name {
			delete(conf.Issuers, iss)
		}
	}
	for iss := range conf.publicKeys {
		if iss.Root() == name {
			delete(conf.publicKeys, iss)
		}
	}
	for cred := range conf.CredentialTypes {
		if cred.Root() == name {
			delete(conf.CredentialTypes, cred)
		}
	}
355
356
357
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

358
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
359
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
360
	conf.publicKeys[issuerid] = map[int]*gabi.PublicKey{}
361
	path := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
362
363
364
365
366
367
368
369
370
371
	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 {
372
			return err
373
		}
Sietse Ringers's avatar
Sietse Ringers committed
374
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
375
		if err != nil || !found {
376
377
378
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
379
380
381
		if err != nil {
			return err
		}
382
383
384
		if int(pk.Counter) != i {
			return errors.Errorf("Public key %s of issuer %s has wrong <Counter>", file, issuerid.String())
		}
385
		pk.Issuer = issuerid.String()
386
		conf.publicKeys[issuerid][i] = pk
387
	}
388

389
390
391
	return nil
}

392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
func (conf *Configuration) PublicKeyIndices(issuerid IssuerIdentifier) (i []int, err error) {
	return conf.matchKeyPattern(issuerid, pubkeyPattern)
}

func (conf *Configuration) matchKeyPattern(issuerid IssuerIdentifier, pattern string) (i []int, err error) {
	pkpath := fmt.Sprintf(pattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
	files, err := filepath.Glob(pkpath)
	if err != nil {
		return
	}
	for _, file := range files {
		var count int
		base := filepath.Base(file)
		if count, err = strconv.Atoi(base[:len(base)-4]); err != nil {
			return
		}
		i = append(i, count)
	}
	sort.Ints(i)
	return
}

414
// parse $schememanager/$issuer/Issues/*/description.xml
415
416
417
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, issuer *Issuer, path string) error {
	var foundcred bool
	err := iterateSubfolders(path, func(dir string) error {
418
		cred := &CredentialType{}
419
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
420
421
422
		if err != nil {
			return err
		}
423
424
425
		if !exists {
			return nil
		}
426
		if err = conf.checkCredentialType(manager, issuer, cred, dir); err != nil {
427
428
429
430
			return err
		}
		foundcred = true
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
431
		credid := cred.Identifier()
432
433
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
434
435
436
437
438
439
440
		for index, attr := range cred.Attributes {
			attr.Index = index
			attr.SchemeManagerID = cred.SchemeManagerID
			attr.IssuerID = cred.IssuerID
			attr.CredentialTypeID = cred.ID
			conf.Attributes[attr.GetAttributeTypeIdentifier()] = attr
		}
441
442
		return nil
	})
443
444
445
446
447
448
	if !foundcred {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no credential types", issuer.Identifier().String()))
	}
	return err
}

449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
// 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
		}
466
467
468
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
469
470
471
472
473
474
475
476
477
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

478
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
479
480
481
482
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
483
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
484
485
486
	if !found {
		return false, nil
	}
487
488
489
490
	if err != nil {
		return true, err
	}

491
	err = xml.Unmarshal(bts, description)
492
493
494
495
496
497
	if err != nil {
		return true, err
	}

	return true, nil
}
498

499
500
501
502
503
// 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
504
}
505

506
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
507
508
509
	if conf.assets == "" {
		return true, nil
	}
510
511
512
513
	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)
514
	}
515
516
517
518
	// 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
519
	}
520
	return exists && !newTime.After(*oldTime), nil
521
522
}

523
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
524
	if conf.assets == "" {
525
		return false, nil
526
	}
527
528
529
530
531
	// 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
532
	}
533
534
535
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
536
537
538
	)
}

539
540
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
541
func DownloadSchemeManager(url string) (*SchemeManager, error) {
542
543
544
545
546
547
548
549
550
	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")]
	}
551
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
552
553
554
	if err != nil {
		return nil, err
	}
555
	manager := NewSchemeManager("")
556
557
558
559
560
561
562
563
	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
564
565
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
566
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
567
	// Remove everything falling under the manager's responsibility
568
	for credid := range conf.CredentialTypes {
569
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
570
			delete(conf.CredentialTypes, credid)
571
572
		}
	}
573
	for issid := range conf.Issuers {
574
		if issid.SchemeManagerIdentifier() == id {
575
			delete(conf.Issuers, issid)
576
577
		}
	}
578
	for issid := range conf.publicKeys {
579
		if issid.SchemeManagerIdentifier() == id {
580
			delete(conf.publicKeys, issid)
581
582
		}
	}
583
	delete(conf.SchemeManagers, id)
584
585

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
586
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
587
588
	}
	return nil
589
590
}

591
592
593
594
595
596
597
598
599
600
601
602
603
604
func (conf *Configuration) ReinstallSchemeManager(manager *SchemeManager) (err error) {
	// Check if downloading stuff from the remote works before we uninstall the specified manager:
	// If we can't download anything we should keep the broken version
	manager, err = DownloadSchemeManager(manager.URL)
	if err != nil {
		return
	}
	if err = conf.DeleteSchemeManager(manager.Identifier()); err != nil {
		return
	}
	err = conf.InstallSchemeManager(manager)
	return
}

605
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
606
// provided its signature is valid.
607
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
608
	name := manager.ID
609
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
610
611
		return err
	}
612
613

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
614
	path := fmt.Sprintf("%s/%s", conf.Path, name)
615
616
617
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
618
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
619
620
		return err
	}
621
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
622
623
		return err
	}
624
625
626
627
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
628

629
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
630
631
632
633
634
635
}

// 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
636
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
637
638
639
640
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
641
		return
642
643
	}
	if err = t.GetFile("index.sig", sig); err != nil {
644
		return
645
	}
646
	err = conf.VerifySignature(manager.Identifier())
647
	return
648
}
649

Sietse Ringers's avatar
Sietse Ringers committed
650
651
652
// 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.
653
func (conf *Configuration) Download(session SessionRequest) (downloaded *IrmaIdentifierSet, err error) {
654
	managers := make(map[string]struct{}) // Managers that we must update
655
	downloaded = &IrmaIdentifierSet{
656
657
658
659
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
660

661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
	// 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
}

681
func (conf *Configuration) checkCredentialTypes(session SessionRequest, managers map[string]struct{}) error {
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
	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 {
737
	for issid := range set.Issuers {
738
		if _, contains := conf.Issuers[issid]; !contains {
739
			managers[issid.Root()] = struct{}{}
740
		}
Sietse Ringers's avatar
Sietse Ringers committed
741
	}
742
743
744
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
745
			if err != nil {
746
				return err
Sietse Ringers's avatar
Sietse Ringers committed
747
748
			}
			if pk == nil {
749
				managers[issid.Root()] = struct{}{}
750
751
752
			}
		}
	}
753
	return nil
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
}

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
775
// FromString populates this index by parsing the specified string.
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
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
}

795
// parseIndex parses the index file of the specified manager.
796
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
797
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
798
	if err := fs.AssertPathExists(path); err != nil {
799
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
800
	}
Sietse Ringers's avatar
Sietse Ringers committed
801
	indexbts, err := ioutil.ReadFile(path)
802
	if err != nil {
803
		return nil, err
804
	}
805
806
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
807
808
}

809
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
810
	err := conf.VerifySignature(manager.Identifier())
811
812
813
814
	if err != nil {
		return err
	}

815
	var exists bool
816
	for file := range manager.index {
817
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
818
819
820
821
822
823
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
824
		// Don't care about the actual bytes
825
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
826
827
828
829
830
831
832
			return err
		}
	}

	return nil
}

833
834
835
// 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.
836
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
837
	signedHash, ok := manager.index[filepath.ToSlash(path)]
838
	if !ok {
839
		return nil, false, nil
840
841
	}

Sietse Ringers's avatar
Sietse Ringers committed
842
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
843
	if err != nil {
844
		return nil, true, err
845
846
847
848
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
849
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
850
	}
851
	return bts, true, nil
852
853
854
855
856
}

// 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).
857
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
858
859
860
861
862
863
864
865
866
867
	defer func() {
		if r := recover(); r != nil {
			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
868
	dir := filepath.Join(conf.Path, id.String())
869
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
870
		return errors.New("Missing scheme manager index file, signature, or public key")
871
872
873
874
875
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
876
		return err
877
878
879
880
881
882
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
883
		return err
884
885
886
887
	}
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
888
		return err
889
890
891
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
892
		return errors.New("Invalid scheme manager public key")
893
894
895
896
897
	}

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

	// Verify signature
904
905
906
907
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
908
}
909
910
911
912

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928

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)
	}

929
930
931
932
933
934
935
936
937
938
	// 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
	}
939
	if !manager.Timestamp.Before(*timestamp) {
940
941
942
		return nil
	}

943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
	// 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 {
961
		path := filepath.Join(conf.Path, filename)
962
		oldHash, known := manager.index[filename]
963
964
965
966
967
968
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
969
970
971
972
973
974
975
976
			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
977
		if err = transport.GetSignedFile(stripped, path, newHash); err != nil {
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
			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
}
1000
1001
1002
1003
1004

// Methods containing consistency checks on irma_configuration

func (conf *Configuration) checkIssuer(manager *SchemeManager, issuer *Issuer, dir string) error {
	issuerid := issuer.Identifier()
1005
1006
	conf.checkTranslations(fmt.Sprintf("Issuer %s", issuerid.String()), issuer)
	// Check that the issuer has public keys
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
	pkpath := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
	files, err := filepath.Glob(pkpath)
	if err != nil {
		return err
	}
	if len(files) == 0 {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no public keys", issuerid.String()))
	}

	if filepath.Base(dir) != issuer.ID {
		return errors.Errorf("Issuer %s has wrong directory name %s", issuerid.String(), filepath.Base(dir))
	}
	if manager.ID != issuer.SchemeManagerID {
		return errors.Errorf("Issuer %s has wrong SchemeManager %s", issuerid.String(), issuer.SchemeManagerID)
	}
	if err = fs.AssertPathExists(dir + "/logo.png"); err != nil {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no logo.png", issuerid.String()))
	}
	return nil
}

func (conf *Configuration) checkCredentialType(manager *SchemeManager, issuer *Issuer, cred *CredentialType, dir string) error {
	credid := cred.Identifier()
1030
	conf.checkTranslations(fmt.Sprintf("Credential type %s", credid.String()), cred)
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
	if cred.XMLVersion < 4 {
		return errors.New("Unsupported credential type description")
	}
	if cred.ID != filepath.Base(dir) {
		return errors.Errorf("Credential type %s has wrong directory name %s", credid.String(), filepath.Base(dir))
	}
	if cred.IssuerID != issuer.ID {
		return errors.Errorf("Credential type %s has wrong IssuerID %s", credid.String(), cred.IssuerID)
	}
	if cred.SchemeManagerID != manager.ID {
		return errors.Errorf("Credential type %s has wrong SchemeManager %s", credid.String(), cred.SchemeManagerID)
	}
	if err := fs.AssertPathExists(dir + "/logo.png"); err != nil {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has no logo.png", credid.String()))
	}
	return conf.checkAttributes(cred)
}

func (conf *Configuration) checkAttributes(cred *CredentialType) error {
	name := cred.Identifier().String()
	indices := make(map[int]struct{})
	count := len(cred.Attributes)
	if count == 0 {
		return errors.Errorf("Credenial type %s has no attributes", name)
	}
	for i, attr := range cred.Attributes {
1057
		conf.checkTranslations(fmt.Sprintf("Attribute %s of credential type %s", attr.ID, cred.Identifier().String()), attr)
1058
		index := i
1059
1060
		if attr.DisplayIndex != nil {
			index = *attr.DisplayIndex
1061
1062
		}
		if index >= count {
1063
			conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has invalid attribute displayIndex at attribute %d", name, i))
1064
1065
1066
1067
		}
		indices[index] = struct{}{}
	}
	if len(indices) != count {
1068
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has invalid attribute ordering, check the displayIndex tags", name))
1069
1070
1071
	}
	return nil
}
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107

func (conf *Configuration) checkScheme(scheme *SchemeManager, dir string) error {
	if scheme.XMLVersion < 7 {
		scheme.Status = SchemeManagerStatusParsingError
		return errors.New("Unsupported scheme manager description")
	}
	if filepath.Base(dir) != scheme.ID {
		scheme.Status = SchemeManagerStatusParsingError
		return errors.Errorf("Scheme %s has wrong directory name %s", scheme.ID, filepath.Base(dir))
	}
	conf.checkTranslations(fmt.Sprintf("Scheme %s", scheme.ID), scheme)
	return nil
}

// checkTranslations checks for each member of the interface o that is of type TranslatedString
// that it contains all necessary translations.
func (conf *Configuration) checkTranslations(file string, o interface{}) {
	langs := []string{"en", "nl"} // Hardcode these for now, TODO make configurable
	v := reflect.ValueOf(o)

	// Dereference in case of pointer or interface
	if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
		v = v.Elem()
	}

	for i := 0; i < v.NumField(); i++ {
		if v.Field(i).Type() == reflect.TypeOf(TranslatedString{}) {
			val := v.Field(i).Interface().(TranslatedString)
			for _, lang := range langs {
				if _, exists := val[lang]; !exists {
					conf.Warnings = append(conf.Warnings, fmt.Sprintf("%s misses %s translation in <%s> tag", file, lang, v.Type().Field(i).Name))
				}
			}
		}
	}
}
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166

func (conf *Configuration) CheckKeys() error {
	const expiryBoundary = int64(time.Hour/time.Second) * 24 * 31 // 1 month, TODO make configurable

	for issuerid := range conf.Issuers {
		if err := conf.parseKeysFolder(conf.SchemeManagers[issuerid.SchemeManagerIdentifier()], issuerid); err != nil {
			return err
		}
		indices, err := conf.PublicKeyIndices(issuerid)
		if err != nil {
			return err
		}
		if len(indices) == 0 {
			continue
		}
		latest, err := conf.PublicKey(issuerid, indices[len(indices)-1])
		now := time.Now().Unix()
		if latest == nil || latest.ExpiryDate < now {
			conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no nonexpired public keys", issuerid.String()))
		}
		if latest != nil && latest.ExpiryDate > now && latest.ExpiryDate < now+expiryBoundary {
			conf.Warnings = append(conf.Warnings, fmt.Sprintf("Latest public key of issuer %s expires soon (at %s)",
				issuerid.String(), time.Unix(latest.ExpiryDate, 0).String()))
		}

		// Check private keys if any
		privkeypath := fmt.Sprintf(privkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
		privkeys, err := filepath.Glob(privkeypath)
		if err != nil {
			return err
		}
		for _, privkey := range privkeys {
			filename := filepath.Base(privkey)
			count,