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

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

11
12
	"crypto/sha256"

13
14
15
16
	"fmt"

	"strings"

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

	"bytes"

	"encoding/hex"

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

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

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

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

47
48
49
50
type ConfigurationFileHash []byte

type SchemeManagerIndex map[string]ConfigurationFileHash

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

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

67
68
69
	return
}

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

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

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

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

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

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

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

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

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

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

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

187
188
189
	return nil
}

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

	return nil
}

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

245
	bts, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.path, path))
246
247
248
249
	if err != nil {
		return true, err
	}

250
	err = xml.Unmarshal(bts, description)
251
252
253
254
255
256
	if err != nil {
		return true, err
	}

	return true, nil
}
257

258
259
260
261
262
// 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
263
}
264

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

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

	if err != nil {
		return err
	}
	if parse {
302
		return conf.ParseFolder()
303
304
	}
	return nil
305
}
306

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

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

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

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

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

373
	conf.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager
374
375
	return nil
}
376

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

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

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

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

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

462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
	for manager := range updatedManagers {
		if err := conf.DownloadSchemeManagerSignature(conf.SchemeManagers[manager]); err != nil {
			return nil, err
		}
	}
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
	return downloaded, nil
}

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

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

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

	return b.String()
}

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

	return nil
}

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

// 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) {
527
	signedHash, ok := manager.Index[path]
528
529
530
531
532
533
534
535
536
537
538
	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) {
539
		return nil, errors.Errorf("Hash of %s does not match scheme manager index", path)
540
541
542
543
544
545
546
	}
	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).
547
548
549
550
551
552
553
554
555
556
557
558
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")
			}
		}
	}()

559
560
561
562
563
564
565
566
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
	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
596
}
597
598
599
600

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