irmaconfig.go 11.3 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
	"github.com/credentials/irmago/internal/fs"
18
	"github.com/go-errors/errors"
19
20
21
	"github.com/mhe/gabi"
)

22
// Configuration keeps track of scheme managers, issuers, credential types and public keys,
23
// dezerializing them from an irma_configuration folder, and downloads and saves new ones on demand.
24
type Configuration struct {
25
26
27
	SchemeManagers  map[SchemeManagerIdentifier]*SchemeManager
	Issuers         map[IssuerIdentifier]*Issuer
	CredentialTypes map[CredentialTypeIdentifier]*CredentialType
28

29
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
30
	reverseHashes map[string]CredentialTypeIdentifier
31
	path          string
32
	initialized   bool
33
34
}

35
// NewConfiguration returns a new configuration. After this
36
// ParseFolder() should be called to parse the specified path.
37
38
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
	conf = &Configuration{
39
		path: path,
40
	}
41

42
	if err = fs.EnsureDirectoryExists(conf.path); err != nil {
43
44
		return nil, err
	}
45
	if assets != "" {
46
		if err = conf.Copy(assets, false); err != nil {
47
48
49
50
			return nil, err
		}
	}

51
52
53
	return
}

54
// ParseFolder populates the current Configuration by parsing the storage path,
55
// listing the containing scheme managers, issuers and credential types.
56
func (conf *Configuration) ParseFolder() error {
57
	// Init all maps
58
59
60
61
	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)
62

63
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
64

65
	err := iterateSubfolders(conf.path, func(dir string) error {
66
67
68
69
70
		manager := &SchemeManager{}
		exists, err := pathToDescription(dir+"/description.xml", manager)
		if err != nil {
			return err
		}
71
72
		if !exists {
			return nil
73
		}
74
75
76
77
78
		if manager.XMLVersion < 7 {
			return errors.New("Unsupported scheme manager description")
		}
		conf.SchemeManagers[manager.Identifier()] = manager
		return conf.parseIssuerFolders(dir)
79
80
81
82
	})
	if err != nil {
		return err
	}
83
	conf.initialized = true
84
85
86
	return nil
}

87
88
89
90
91
// 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{}
		if err := conf.parseKeysFolder(id); err != nil {
92
			return nil, err
93
94
		}
	}
95
	return conf.publicKeys[id][counter], nil
96
97
}

98
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
99
	hash := sha256.Sum256([]byte(credid.String()))
100
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
101
102
}

103
104
105
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
106
107
108
109
	}
	return nil
}

110
// IsInitialized indicates whether this instance has successfully been initialized.
111
112
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
113
114
}

115
func (conf *Configuration) parseIssuerFolders(path string) error {
116
117
118
119
120
121
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
		exists, err := pathToDescription(dir+"/description.xml", issuer)
		if err != nil {
			return err
		}
122
123
		if !exists {
			return nil
124
		}
125
126
127
128
129
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
		conf.Issuers[issuer.Identifier()] = issuer
		return conf.parseCredentialsFolder(dir + "/Issues/")
130
131
132
	})
}

133
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
134
135
func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
	path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
136
137
138
139
140
141
142
143
144
145
146
	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
147
148
149
150
151
		}
		pk, err := gabi.NewPublicKeyFromFile(file)
		if err != nil {
			return err
		}
152
		pk.Issuer = issuerid.String()
153
		conf.publicKeys[issuerid][i] = pk
154
	}
155

156
157
158
	return nil
}

159
// parse $schememanager/$issuer/Issues/*/description.xml
160
func (conf *Configuration) parseCredentialsFolder(path string) error {
161
162
163
164
165
166
	return iterateSubfolders(path, func(dir string) error {
		cred := &CredentialType{}
		exists, err := pathToDescription(dir+"/description.xml", cred)
		if err != nil {
			return err
		}
167
168
169
170
171
		if !exists {
			return nil
		}
		if cred.XMLVersion < 4 {
			return errors.New("Unsupported credential type description")
172
		}
173
174
175
		credid := cred.Identifier()
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
		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
		}
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

func pathToDescription(path string, description interface{}) (bool, error) {
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

	file, err := os.Open(path)
	if err != nil {
		return true, err
	}
	defer file.Close()

	bytes, err := ioutil.ReadAll(file)
	if err != nil {
		return true, err
	}

	err = xml.Unmarshal(bytes, description)
	if err != nil {
		return true, err
	}

	return true, nil
}
229

230
231
232
233
234
// 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
235
}
236

237
238
func (conf *Configuration) Copy(source string, parse bool) error {
	if err := fs.EnsureDirectoryExists(conf.path); err != nil {
239
240
241
		return err
	}

242
	err := filepath.Walk(source, filepath.WalkFunc(
243
244
245
246
247
248
		func(path string, info os.FileInfo, err error) error {
			if path == source {
				return nil
			}
			subpath := path[len(source):]
			if info.IsDir() {
249
				if err := fs.EnsureDirectoryExists(conf.path + subpath); err != nil {
250
251
252
253
254
255
256
257
					return err
				}
			} else {
				srcfile, err := os.Open(path)
				if err != nil {
					return err
				}
				defer srcfile.Close()
Sietse Ringers's avatar
Sietse Ringers committed
258
259
260
261
				bytes, err := ioutil.ReadAll(srcfile)
				if err != nil {
					return err
				}
262
				if err := fs.SaveFile(conf.path+subpath, bytes); err != nil {
263
264
265
266
267
268
					return err
				}
			}
			return nil
		}),
	)
269
270
271
272
273

	if err != nil {
		return err
	}
	if parse {
274
		return conf.ParseFolder()
275
276
	}
	return nil
277
}
278

279
280
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
281
func (conf *Configuration) DownloadSchemeManager(url string) (*SchemeManager, error) {
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
	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
}

304
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier) error {
305
	// Remove everything falling under the manager's responsibility
306
	for credid := range conf.CredentialTypes {
307
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
308
			delete(conf.CredentialTypes, credid)
309
310
		}
	}
311
	for issid := range conf.Issuers {
312
		if issid.SchemeManagerIdentifier() == id {
313
			delete(conf.Issuers, issid)
314
315
		}
	}
316
	for issid := range conf.publicKeys {
317
		if issid.SchemeManagerIdentifier() == id {
318
			delete(conf.publicKeys, issid)
319
320
		}
	}
321
	delete(conf.SchemeManagers, id)
322
	// Remove from storage
323
	return os.RemoveAll(fmt.Sprintf("%s/%s", conf.path, id.String()))
324
325
326
	// or, remove above iterations and call .ParseFolder()?
}

327
func (conf *Configuration) AddSchemeManager(manager *SchemeManager) error {
328
	name := manager.ID
329
	if err := fs.EnsureDirectoryExists(fmt.Sprintf("%s/%s", conf.path, name)); err != nil {
330
331
332
333
334
335
		return err
	}
	b, err := xml.Marshal(manager)
	if err != nil {
		return err
	}
336
	if err := fs.SaveFile(fmt.Sprintf("%s/%s/description.xml", conf.path, name), b); err != nil {
337
338
		return err
	}
339
	conf.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager
340
341
	return nil
}
342

343
func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, error) {
344
	var contains bool
345
346
347
348
349
350
351
	var err error
	downloaded := &IrmaIdentifierSet{
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}

352
	for manid := range set.SchemeManagers {
353
		if _, contains = conf.SchemeManagers[manid]; !contains {
354
			return nil, errors.Errorf("Unknown scheme manager: %s", manid)
355
356
357
358
359
		}
	}

	transport := NewHTTPTransport("")
	for issid := range set.Issuers {
360
361
362
		if _, contains = conf.Issuers[issid]; !contains {
			url := conf.SchemeManagers[issid.SchemeManagerIdentifier()].URL + "/" + issid.Name()
			path := fmt.Sprintf("%s/%s/%s", conf.path, issid.SchemeManagerIdentifier().String(), issid.Name())
363
364
365
			if err = transport.GetFile(url+"/description.xml", path+"/description.xml"); err != nil {
				return nil, err
			}
366
			if err = transport.GetFile(url+"/logo.png", path+"/logo.png"); err != nil {
367
368
369
				return nil, err
			}
			downloaded.Issuers[issid] = struct{}{}
370
		}
Sietse Ringers's avatar
Sietse Ringers committed
371
372
373
	}
	for issid, list := range set.PublicKeys {
		for _, count := range list {
374
			pk, err := conf.PublicKey(issid, count)
Sietse Ringers's avatar
Sietse Ringers committed
375
376
377
378
379
380
			if err != nil {
				return nil, err
			}
			if pk == nil {
				manager := issid.SchemeManagerIdentifier()
				suffix := fmt.Sprintf("/%s/PublicKeys/%d.xml", issid.Name(), count)
381
				path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), suffix)
382
				if err = transport.GetFile(conf.SchemeManagers[manager].URL+suffix, path); err != nil {
383
					return nil, err
384
				}
385
386
387
388
			}
		}
	}
	for credid := range set.CredentialTypes {
389
		if _, contains := conf.CredentialTypes[credid]; !contains {
390
391
			issuer := credid.IssuerIdentifier()
			manager := issuer.SchemeManagerIdentifier()
392
			local := fmt.Sprintf("%s/%s/%s/Issues", conf.path, manager.Name(), issuer.Name())
393
			if err := fs.EnsureDirectoryExists(local); err != nil {
394
				return nil, err
395
			}
396
397
			if err = transport.GetFile(
				fmt.Sprintf("%s/%s/Issues/%s/description.xml", conf.SchemeManagers[manager].URL, issuer.Name(), credid.Name()),
398
				fmt.Sprintf("%s/%s/description.xml", local, credid.Name()),
399
400
401
			); err != nil {
				return nil, err
			}
402
403
404
405
			_ = 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()),
			)
406
			downloaded.CredentialTypes[credid] = struct{}{}
407
408
409
		}
	}

410
	return downloaded, conf.ParseFolder()
411
}