api.go 18.2 KB
Newer Older
Sietse Ringers's avatar
Sietse Ringers committed
1
// Package servercore is the core of the IRMA server library, allowing IRMA verifiers, issuers
2
3
// or attribute-based signature applications to perform IRMA sessions with irmaclient instances
// (i.e. the IRMA app). It exposes a small interface to expose to other programming languages
Sietse Ringers's avatar
Sietse Ringers committed
4
// through cgo. It is used by the irmaserver package but otherwise not meant for use in Go.
5
package servercore
6
7
8

import (
	"encoding/json"
9
	"io/ioutil"
10
	"net/http"
11
	"path/filepath"
12
	"regexp"
13
	"strconv"
14
	"strings"
15
	"time"
16
17

	"github.com/go-errors/errors"
18
	"github.com/jasonlvhit/gocron"
19
20
	"github.com/privacybydesign/gabi"
	"github.com/privacybydesign/gabi/big"
21
	"github.com/privacybydesign/gabi/revocation"
22
	"github.com/privacybydesign/irmago"
23
	"github.com/privacybydesign/irmago/internal/fs"
Sietse Ringers's avatar
Sietse Ringers committed
24
	"github.com/privacybydesign/irmago/server"
25
	"github.com/sirupsen/logrus"
26
27
)

28
type Server struct {
29
30
31
32
	conf          *server.Configuration
	sessions      sessionStore
	scheduler     *gocron.Scheduler
	stopScheduler chan bool
33
34
35
36
37
38
39
}

func New(conf *server.Configuration) (*Server, error) {
	s := &Server{
		conf:      conf,
		scheduler: gocron.NewScheduler(),
		sessions: &memorySessionStore{
40
41
42
			requestor: make(map[string]*session),
			client:    make(map[string]*session),
			conf:      conf,
43
44
45
46
47
		},
	}
	s.scheduler.Every(10).Seconds().Do(func() {
		s.sessions.deleteExpired()
	})
48
	s.stopScheduler = s.scheduler.Start()
49
50
51

	return s, s.verifyConfiguration(s.conf)
}
52

53
func (s *Server) Stop() {
54
55
56
	if err := s.conf.IrmaConfiguration.Close(); err != nil {
		_ = server.LogWarning(err)
	}
57
58
59
60
	s.stopScheduler <- true
	s.sessions.stop()
}

61
func (s *Server) verifyIrmaConf(configuration *server.Configuration) error {
62
	if s.conf.IrmaConfiguration == nil {
63
64
65
66
67
		var (
			err    error
			exists bool
		)
		if s.conf.SchemesPath == "" {
68
			s.conf.SchemesPath = irma.DefaultSchemesPath() // Returns an existing path
69
70
71
72
73
		}
		if exists, err = fs.PathExists(s.conf.SchemesPath); err != nil {
			return server.LogError(err)
		}
		if !exists {
74
			return server.LogError(errors.Errorf("Nonexisting schemes_path provided: %s", s.conf.SchemesPath))
75
		}
76
		s.conf.Logger.WithField("schemes_path", s.conf.SchemesPath).Info("Determined schemes path")
77
78
		if s.conf.SchemesAssetsPath == "" {
			s.conf.IrmaConfiguration, err = irma.NewConfiguration(s.conf.SchemesPath)
79
		} else {
80
			s.conf.IrmaConfiguration, err = irma.NewConfigurationFromAssets(s.conf.SchemesPath, s.conf.SchemesAssetsPath)
81
		}
82
		if err != nil {
83
			return server.LogError(err)
84
		}
85
		if err = s.conf.IrmaConfiguration.ParseFolder(); err != nil {
86
			return server.LogError(err)
87
		}
88
89
90
91
		if err = fs.EnsureDirectoryExists(s.conf.RevocationPath); err != nil {
			return server.LogError(err)
		}
		s.conf.IrmaConfiguration.RevocationPath = s.conf.RevocationPath
92
93
	}

94
	if len(s.conf.IrmaConfiguration.SchemeManagers) == 0 {
95
96
97
		s.conf.Logger.Infof("No schemes found in %s, downloading default (irma-demo and pbdf)", s.conf.SchemesPath)
		if err := s.conf.IrmaConfiguration.DownloadDefaultSchemes(); err != nil {
			return server.LogError(err)
98
		}
99
	}
100

101
	if !s.conf.DisableSchemesUpdate {
102
103
104
		if s.conf.SchemesUpdateInterval == 0 {
			s.conf.SchemesUpdateInterval = 60
		}
105
		s.conf.IrmaConfiguration.AutoUpdateSchemes(uint(s.conf.SchemesUpdateInterval))
106
107
	} else {
		s.conf.SchemesUpdateInterval = 0
Sietse Ringers's avatar
Sietse Ringers committed
108
109
	}

110
111
112
113
	return nil
}

func (s *Server) verifyPrivateKeys(configuration *server.Configuration) error {
114
115
	if s.conf.IssuerPrivateKeys == nil {
		s.conf.IssuerPrivateKeys = make(map[irma.IssuerIdentifier]*gabi.PrivateKey)
116
	}
117
118
	if s.conf.IssuerPrivateKeysPath != "" {
		files, err := ioutil.ReadDir(s.conf.IssuerPrivateKeysPath)
119
		if err != nil {
120
			return server.LogError(err)
121
122
123
		}
		for _, file := range files {
			filename := file.Name()
124
125
			if filepath.Ext(filename) != ".xml" || filename[0] == '.' || strings.Count(filename, ".") != 2 {
				s.conf.Logger.WithField("file", filename).Infof("Skipping non-private key file encountered in private keys path")
126
127
				continue
			}
128
			issid := irma.NewIssuerIdentifier(strings.TrimSuffix(filename, filepath.Ext(filename))) // strip .xml
129
			if _, ok := s.conf.IrmaConfiguration.Issuers[issid]; !ok {
130
				return server.LogError(errors.Errorf("Private key %s belongs to an unknown issuer", filename))
131
			}
132
			sk, err := gabi.NewPrivateKeyFromFile(filepath.Join(s.conf.IssuerPrivateKeysPath, filename))
133
			if err != nil {
134
				return server.LogError(err)
135
			}
136
			s.conf.IssuerPrivateKeys[issid] = sk
137
138
		}
	}
139
140
	for issid, sk := range s.conf.IssuerPrivateKeys {
		pk, err := s.conf.IrmaConfiguration.PublicKey(issid, int(sk.Counter))
141
		if err != nil {
142
			return server.LogError(err)
143
144
		}
		if pk == nil {
145
			return server.LogError(errors.Errorf("Missing public key belonging to private key %s-%d", issid.String(), sk.Counter))
146
147
		}
		if new(big.Int).Mul(sk.P, sk.Q).Cmp(pk.N) != 0 {
148
			return server.LogError(errors.Errorf("Private key %s-%d does not belong to corresponding public key", issid.String(), sk.Counter))
149
150
		}
	}
151
152
153
154
155
156
157
158
159
160
161

	return nil
}

func (s *Server) verifyRevocation(configuration *server.Configuration) error {
	for credid, settings := range s.conf.RevocationServers {
		if _, known := s.conf.IrmaConfiguration.CredentialTypes[credid]; !known {
			return server.LogError(errors.Errorf("unknown credential type %s in revocation settings", credid))
		}

		db, err := s.conf.IrmaConfiguration.RevocationDB(credid)
162
163
164
		if err != nil {
			return server.LogError(err)
		}
165
166
167
168
169
170
171

		db.OnChange(func(record *revocation.Record) {
			transport := irma.NewHTTPTransport("")
			o := struct{}{}
			for _, url := range settings.PostURLs {
				if err := transport.Post(url+"/-/revocation/records", &o, &[]*revocation.Record{record}); err != nil {
					s.conf.Logger.Warn("error sending revocation update", err)
172
				}
173
			}
174
		})
175
	}
176

177
178
179
180
	return nil
}

func (s *Server) verifyURL(configuration *server.Configuration) error {
181
182
183
	if s.conf.URL != "" {
		if !strings.HasSuffix(s.conf.URL, "/") {
			s.conf.URL = s.conf.URL + "/"
184
		}
185
186
187
188
189
190
191
192
193
194
		if !strings.HasPrefix(s.conf.URL, "https://") {
			if !s.conf.Production || s.conf.DisableTLS {
				s.conf.DisableTLS = true
				s.conf.Logger.Warnf("TLS is not enabled on the url \"%s\" to which the IRMA app will connect. "+
					"Ensure that attributes are encrypted in transit by either enabling TLS or adding TLS in a reverse proxy.", s.conf.URL)
			} else {
				return server.LogError(errors.Errorf("Running without TLS in production mode is unsafe without a reverse proxy. " +
					"Either use a https:// URL or explicitly disable TLS."))
			}
		}
195
	} else {
196
		s.conf.Logger.Warn("No url parameter specified in configuration; unless an url is elsewhere prepended in the QR, the IRMA client will not be able to connect")
197
	}
198
199
	return nil
}
200

201
func (s *Server) verifyEmail(configuration *server.Configuration) error {
Sietse Ringers's avatar
Sietse Ringers committed
202
203
204
205
206
	if s.conf.Email != "" {
		// Very basic sanity checks
		if !strings.Contains(s.conf.Email, "@") || strings.Contains(s.conf.Email, "\n") {
			return server.LogError(errors.New("Invalid email address specified"))
		}
Sietse Ringers's avatar
Sietse Ringers committed
207
		t := irma.NewHTTPTransport("https://metrics.privacybydesign.foundation/history")
Sietse Ringers's avatar
Sietse Ringers committed
208
209
		t.SetHeader("User-Agent", "irmaserver")
		var x string
Sietse Ringers's avatar
Sietse Ringers committed
210
		_ = t.Post("email", &x, s.conf.Email)
Sietse Ringers's avatar
Sietse Ringers committed
211
	}
212
213
214
215
216
217
218
219
220
221
222
	return nil
}

func (s *Server) verifyConfiguration(configuration *server.Configuration) error {
	if s.conf.Logger == nil {
		s.conf.Logger = server.NewLogger(s.conf.Verbose, s.conf.Quiet, s.conf.LogJSON)
	}
	server.Logger = s.conf.Logger
	irma.Logger = s.conf.Logger

	// loop to avoid repetetive err != nil line triplets
223
224
225
	for _, f := range []func(*server.Configuration) error{
		s.verifyIrmaConf, s.verifyPrivateKeys, s.verifyRevocation, s.verifyURL, s.verifyEmail,
	} {
226
227
228
229
		if err := f(configuration); err != nil {
			return err
		}
	}
Sietse Ringers's avatar
Sietse Ringers committed
230

231
232
233
	return nil
}

234
func (s *Server) validateRequest(request irma.SessionRequest) error {
235
236
237
238
	if _, err := s.conf.IrmaConfiguration.Download(request); err != nil {
		return err
	}
	return request.Disclosure().Disclose.Validate(s.conf.IrmaConfiguration)
239
240
}

241
func (s *Server) StartSession(req interface{}) (*irma.Qr, string, error) {
242
243
	rrequest, err := server.ParseSessionRequest(req)
	if err != nil {
244
		return nil, "", err
245
	}
246
247
248

	request := rrequest.SessionRequest()
	action := request.Action()
249

Leon's avatar
Leon committed
250
251
252
253
	if err := s.validateRequest(request); err != nil {
		return nil, "", err
	}

254
	if action == irma.ActionIssuing {
255
		if err := s.validateIssuanceRequest(request.(*irma.IssuanceRequest)); err != nil {
256
			return nil, "", err
257
258
259
		}
	}

260
261
262
263
	session := s.newSession(action, rrequest)
	s.conf.Logger.WithFields(logrus.Fields{"action": action, "session": session.token}).Infof("Session started")
	if s.conf.Logger.IsLevelEnabled(logrus.DebugLevel) {
		s.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Info("Session request: ", server.ToJson(rrequest))
264
	} else {
265
		s.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Info("Session request (purged of attribute values): ", server.ToJson(purgeRequest(rrequest)))
266
	}
267
268
	return &irma.Qr{
		Type: action,
269
		URL:  s.conf.URL + "session/" + session.clientToken,
270
271
272
	}, session.token, nil
}

273
274
func (s *Server) GetSessionResult(token string) *server.SessionResult {
	session := s.sessions.get(token)
275
	if session == nil {
276
		s.conf.Logger.Warn("Session result requested of unknown session ", token)
Sietse Ringers's avatar
Sietse Ringers committed
277
278
279
280
281
		return nil
	}
	return session.result
}

282
283
func (s *Server) GetRequest(token string) irma.RequestorRequest {
	session := s.sessions.get(token)
284
	if session == nil {
285
		s.conf.Logger.Warn("Session request requested of unknown session ", token)
286
287
288
289
290
		return nil
	}
	return session.rrequest
}

291
292
func (s *Server) CancelSession(token string) error {
	session := s.sessions.get(token)
293
	if session == nil {
294
		return server.LogError(errors.Errorf("can't cancel unknown session %s", token))
295
296
297
298
299
	}
	session.handleDelete()
	return nil
}

300
301
302
303
304
func ParsePath(path string) (token, noun string, arg []string, err error) {
	client := regexp.MustCompile("session/(\\w+)/?(|commitments|proofs|status|statusevents)$")
	matches := client.FindStringSubmatch(path)
	if len(matches) == 3 {
		return matches[1], matches[2], nil, nil
305
	}
306
307
308
309
310
311
312
313
314

	rev := regexp.MustCompile("-/revocation/(records)/?(.*)$")
	matches = rev.FindStringSubmatch(path)
	if len(matches) == 3 {
		args := strings.Split(matches[2], "/")
		return "", matches[1], args, nil
	}

	return "", "", nil, server.LogWarning(errors.Errorf("Invalid URL: %s", path))
315
316
}

317
func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, token string, requestor bool) error {
318
319
320
321
	if !s.conf.EnableSSE {
		return errors.New("Server sent events disabled")
	}

322
323
324
325
326
327
	var session *session
	if requestor {
		session = s.sessions.get(token)
	} else {
		session = s.sessions.clientGet(token)
	}
328
329
330
331
332
333
334
335
336
	if session == nil {
		return server.LogError(errors.Errorf("can't subscribe to server sent events of unknown session %s", token))
	}
	if session.status.Finished() {
		return server.LogError(errors.Errorf("can't subscribe to server sent events of finished session %s", token))
	}

	session.Lock()
	defer session.Unlock()
337
338
339
340
341
342
343
344
345
346
347
348
349

	// The EventSource.onopen Javascript callback is not consistently called across browsers (Chrome yes, Firefox+Safari no).
	// However, when the SSE connection has been opened the webclient needs some signal so that it can early detect SSE failures.
	// So we manually send an "open" event. Unfortunately:
	// - we need to give the webclient that connected just now some time, otherwise it will miss the "open" event
	// - the "open" event also goes to all other webclients currently listening, as we have no way to send this
	//   event to just the webclient currently listening. (Thus the handler of this "open" event must be idempotent.)
	evtSource := session.eventSource()
	go func() {
		time.Sleep(200 * time.Millisecond)
		evtSource.SendEventMessage("", "open", "")
	}()
	evtSource.ServeHTTP(w, r)
350
351
352
	return nil
}

353
func (s *Server) HandleProtocolMessage(
354
355
356
357
	path string,
	method string,
	headers map[string][]string,
	message []byte,
358
359
360
361
) (int, []byte, *server.SessionResult) {
	var start time.Time
	if s.conf.Verbose >= 2 {
		start = time.Now()
362
		server.LogRequest("client", method, path, "", http.Header(headers), message)
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
	}

	status, output, result := s.handleProtocolMessage(path, method, headers, message)

	if s.conf.Verbose >= 2 {
		server.LogResponse(status, time.Now().Sub(start), output)
	}

	return status, output, result
}

func (s *Server) handleProtocolMessage(
	path string,
	method string,
	headers map[string][]string,
	message []byte,
Sietse Ringers's avatar
Sietse Ringers committed
379
) (status int, output []byte, result *server.SessionResult) {
380
381
382
383
384
385
386
387
388
	// Parse path into session and action
	if len(path) > 0 { // Remove any starting and trailing slash
		if path[0] == '/' {
			path = path[1:]
		}
		if path[len(path)-1] == '/' {
			path = path[:len(path)-1]
		}
	}
389

390
	token, noun, args, err := ParsePath(path)
391
392
	if err != nil {
		status, output = server.JsonResponse(nil, server.RemoteError(server.ErrorUnsupported, ""))
393
394
	}

395
396
397
398
399
400
401
402
403
404
405
	if token != "" {
		status, output, result = s.handleClientMessage(token, noun, method, headers, message)
	} else {
		status, output = s.handleRevocationMessage(noun, method, args, headers, message)
	}
	return
}

func (s *Server) handleClientMessage(
	token, noun, method string, headers map[string][]string, message []byte,
) (status int, output []byte, result *server.SessionResult) {
Sietse Ringers's avatar
Sietse Ringers committed
406
	// Fetch the session
407
	session := s.sessions.clientGet(token)
408
	if session == nil {
409
		s.conf.Logger.WithField("clientToken", token).Warn("Session not found")
Sietse Ringers's avatar
Sietse Ringers committed
410
		status, output = server.JsonResponse(nil, server.RemoteError(server.ErrorSessionUnknown, ""))
411
		return
412
	}
413
414
	session.Lock()
	defer session.Unlock()
415

416
417
	// However we return, if the session status has been updated
	// then we should inform the user by returning a SessionResult
418
	defer func() {
419
420
		if session.status != session.prevStatus {
			session.prevStatus = session.status
421
422
423
424
			result = session.result
		}
	}()

425
	// Route to handler
426
	switch len(noun) {
427
	case 0:
428
		if method == http.MethodDelete {
429
430
431
			session.handleDelete()
			status = http.StatusOK
			return
432
		}
433
		if method == http.MethodGet {
434
435
436
437
			status, output = session.checkCache(message, server.StatusConnected)
			if len(output) != 0 {
				return
			}
438
439
440
441
			h := http.Header(headers)
			min := &irma.ProtocolVersion{}
			max := &irma.ProtocolVersion{}
			if err := json.Unmarshal([]byte(h.Get(irma.MinVersionHeader)), min); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
442
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
443
				return
444
445
			}
			if err := json.Unmarshal([]byte(h.Get(irma.MaxVersionHeader)), max); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
446
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
447
				return
448
			}
Sietse Ringers's avatar
Sietse Ringers committed
449
			status, output = server.JsonResponse(session.handleGetRequest(min, max))
450
			session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusConnected}
451
			return
452
		}
Sietse Ringers's avatar
Sietse Ringers committed
453
		status, output = server.JsonResponse(nil, session.fail(server.ErrorInvalidRequest, ""))
454
		return
455

456
	default:
457
458
459
460
461
462
		if noun == "statusevents" {
			err := server.RemoteError(server.ErrorInvalidRequest, "server sent events not supported by this server")
			status, output = server.JsonResponse(nil, err)
			return
		}

463
464
		if method == http.MethodGet && noun == "status" {
			status, output = server.JsonResponse(session.handleGetStatus())
Sietse Ringers's avatar
Sietse Ringers committed
465
			return
466
467
468
		}

		// Below are only POST enpoints
469
		if method != http.MethodPost {
Sietse Ringers's avatar
Sietse Ringers committed
470
			status, output = server.JsonResponse(nil, session.fail(server.ErrorInvalidRequest, ""))
Sietse Ringers's avatar
Sietse Ringers committed
471
472
473
			return
		}

474
		if noun == "commitments" && session.action == irma.ActionIssuing {
475
476
477
478
			status, output = session.checkCache(message, server.StatusDone)
			if len(output) != 0 {
				return
			}
Sietse Ringers's avatar
Sietse Ringers committed
479
			commitments := &irma.IssueCommitmentMessage{}
480
481
			if err = irma.UnmarshalValidate(message, commitments); err != nil {
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
482
				return
483
			}
Sietse Ringers's avatar
Sietse Ringers committed
484
			status, output = server.JsonResponse(session.handlePostCommitments(commitments))
485
			session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
Sietse Ringers's avatar
Sietse Ringers committed
486
487
			return
		}
488

489
		if noun == "proofs" && session.action == irma.ActionDisclosing {
490
491
492
493
494
495
496
			status, output = session.checkCache(message, server.StatusDone)
			if len(output) != 0 {
				return
			}
			disclosure := &irma.Disclosure{}
			if err = irma.UnmarshalValidate(message, disclosure); err != nil {
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
497
				return
498
			}
Sietse Ringers's avatar
Sietse Ringers committed
499
			status, output = server.JsonResponse(session.handlePostDisclosure(disclosure))
500
			session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
Sietse Ringers's avatar
Sietse Ringers committed
501
502
			return
		}
503

504
		if noun == "proofs" && session.action == irma.ActionSigning {
505
506
507
508
			status, output = session.checkCache(message, server.StatusDone)
			if len(output) != 0 {
				return
			}
Sietse Ringers's avatar
Sietse Ringers committed
509
			signature := &irma.SignedMessage{}
510
511
			if err = irma.UnmarshalValidate(message, signature); err != nil {
				status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
512
				return
513
			}
Sietse Ringers's avatar
Sietse Ringers committed
514
			status, output = server.JsonResponse(session.handlePostSignature(signature))
515
			session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
516
			return
517
		}
Sietse Ringers's avatar
Sietse Ringers committed
518

Sietse Ringers's avatar
Sietse Ringers committed
519
		status, output = server.JsonResponse(nil, session.fail(server.ErrorInvalidRequest, ""))
520
		return
521
522
	}
}
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551

func (s *Server) handleRevocationMessage(
	noun, method string, args []string, headers map[string][]string, message []byte,
) (int, []byte) {
	if noun == "records" && method == http.MethodGet {
		if len(args) != 2 {
			return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, "GET records expects 2 url arguments"))
		}
		index, err := strconv.Atoi(args[1])
		if err != nil {
			return server.JsonResponse(nil, server.RemoteError(server.ErrorMalformedInput, err.Error()))
		}
		cred := irma.NewCredentialTypeIdentifier(args[0])
		return server.JsonResponse(s.handleGetRevocationRecords(cred, index))
	}
	if noun == "records" && method == http.MethodPost {
		if len(args) != 1 {
			return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, "POST records expects 1 url arguments"))
		}
		cred := irma.NewCredentialTypeIdentifier(args[0])
		var records []*revocation.Record
		if err := json.Unmarshal(message, &records); err != nil {
			return server.JsonResponse(nil, server.RemoteError(server.ErrorMalformedInput, err.Error()))
		}
		return server.JsonResponse(s.handlePostRevocationRecords(cred, records))
	}

	return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, ""))
}