api.go 19.3 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 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569

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

func (s *Server) handlePostRevocationRecords(
	cred irma.CredentialTypeIdentifier, records []*revocation.Record,
) (interface{}, *irma.RemoteError) {
	db, err := s.conf.IrmaConfiguration.RevocationDB(cred)
	if err != nil {
		return nil, server.RemoteError(server.ErrorUnknown, err.Error()) // TODO error type
	}
	for _, r := range records {
		if err = db.Add(r.Message, r.PublicKeyIndex); err != nil {
			return nil, server.RemoteError(server.ErrorUnknown, err.Error()) // TODO error type
		}
	}
	return nil, nil
}

func (s *Server) handleGetRevocationRecords(
	cred irma.CredentialTypeIdentifier, index int,
570 571
) ([]*revocation.Record, *irma.RemoteError) {
	if _, ok := s.conf.RevocationServers[cred]; ok {
572 573 574 575 576 577 578 579 580 581 582 583
		return nil, server.RemoteError(server.ErrorInvalidRequest, "not supported by this server")
	}
	db, err := s.conf.IrmaConfiguration.RevocationDB(cred)
	if err != nil {
		return nil, server.RemoteError(server.ErrorUnknown, err.Error()) // TODO error type
	}
	records, err := db.RevocationRecords(index)
	if err != nil {
		return nil, server.RemoteError(server.ErrorUnknown, err.Error()) // TODO error type
	}
	return records, nil
}