Commit 0c5fb131 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

refactor: move server.Configuration consistency checks from internal/servercore to server

parent 2921f80b
...@@ -6,9 +6,7 @@ package servercore ...@@ -6,9 +6,7 @@ package servercore
import ( import (
"encoding/json" "encoding/json"
"io/ioutil"
"net/http" "net/http"
"path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
...@@ -16,11 +14,8 @@ import ( ...@@ -16,11 +14,8 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jasonlvhit/gocron" "github.com/jasonlvhit/gocron"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation" "github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs"
"github.com/privacybydesign/irmago/server" "github.com/privacybydesign/irmago/server"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
...@@ -47,7 +42,7 @@ func New(conf *server.Configuration) (*Server, error) { ...@@ -47,7 +42,7 @@ func New(conf *server.Configuration) (*Server, error) {
}) })
s.stopScheduler = s.scheduler.Start() s.stopScheduler = s.scheduler.Start()
return s, s.verifyConfiguration(s.conf) return s, s.conf.Check()
} }
func (s *Server) Stop() { func (s *Server) Stop() {
...@@ -58,179 +53,6 @@ func (s *Server) Stop() { ...@@ -58,179 +53,6 @@ func (s *Server) Stop() {
s.sessions.stop() s.sessions.stop()
} }
func (s *Server) verifyIrmaConf(configuration *server.Configuration) error {
if s.conf.IrmaConfiguration == nil {
var (
err error
exists bool
)
if s.conf.SchemesPath == "" {
s.conf.SchemesPath = irma.DefaultSchemesPath() // Returns an existing path
}
if exists, err = fs.PathExists(s.conf.SchemesPath); err != nil {
return server.LogError(err)
}
if !exists {
return server.LogError(errors.Errorf("Nonexisting schemes_path provided: %s", s.conf.SchemesPath))
}
s.conf.Logger.WithField("schemes_path", s.conf.SchemesPath).Info("Determined schemes path")
if s.conf.SchemesAssetsPath == "" {
s.conf.IrmaConfiguration, err = irma.NewConfiguration(s.conf.SchemesPath)
} else {
s.conf.IrmaConfiguration, err = irma.NewConfigurationFromAssets(s.conf.SchemesPath, s.conf.SchemesAssetsPath)
}
if err != nil {
return server.LogError(err)
}
if err = s.conf.IrmaConfiguration.ParseFolder(); err != nil {
return server.LogError(err)
}
if err = fs.EnsureDirectoryExists(s.conf.RevocationPath); err != nil {
return server.LogError(err)
}
s.conf.IrmaConfiguration.RevocationPath = s.conf.RevocationPath
}
if len(s.conf.IrmaConfiguration.SchemeManagers) == 0 {
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)
}
}
if !s.conf.DisableSchemesUpdate {
if s.conf.SchemesUpdateInterval == 0 {
s.conf.SchemesUpdateInterval = 60
}
s.conf.IrmaConfiguration.AutoUpdateSchemes(uint(s.conf.SchemesUpdateInterval))
} else {
s.conf.SchemesUpdateInterval = 0
}
return nil
}
func (s *Server) verifyPrivateKeys(configuration *server.Configuration) error {
if s.conf.IssuerPrivateKeys == nil {
s.conf.IssuerPrivateKeys = make(map[irma.IssuerIdentifier]*gabi.PrivateKey)
}
if s.conf.IssuerPrivateKeysPath != "" {
files, err := ioutil.ReadDir(s.conf.IssuerPrivateKeysPath)
if err != nil {
return server.LogError(err)
}
for _, file := range files {
filename := file.Name()
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")
continue
}
issid := irma.NewIssuerIdentifier(strings.TrimSuffix(filename, filepath.Ext(filename))) // strip .xml
if _, ok := s.conf.IrmaConfiguration.Issuers[issid]; !ok {
return server.LogError(errors.Errorf("Private key %s belongs to an unknown issuer", filename))
}
sk, err := gabi.NewPrivateKeyFromFile(filepath.Join(s.conf.IssuerPrivateKeysPath, filename))
if err != nil {
return server.LogError(err)
}
s.conf.IssuerPrivateKeys[issid] = sk
}
}
for issid, sk := range s.conf.IssuerPrivateKeys {
pk, err := s.conf.IrmaConfiguration.PublicKey(issid, int(sk.Counter))
if err != nil {
return server.LogError(err)
}
if pk == nil {
return server.LogError(errors.Errorf("Missing public key belonging to private key %s-%d", issid.String(), sk.Counter))
}
if new(big.Int).Mul(sk.P, sk.Q).Cmp(pk.N) != 0 {
return server.LogError(errors.Errorf("Private key %s-%d does not belong to corresponding public key", issid.String(), sk.Counter))
}
}
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)
if err != nil {
return server.LogError(err)
}
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)
}
}
})
}
return nil
}
func (s *Server) verifyURL(configuration *server.Configuration) error {
if s.conf.URL != "" {
if !strings.HasSuffix(s.conf.URL, "/") {
s.conf.URL = s.conf.URL + "/"
}
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."))
}
}
} else {
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")
}
return nil
}
func (s *Server) verifyEmail(configuration *server.Configuration) error {
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"))
}
t := irma.NewHTTPTransport("https://metrics.privacybydesign.foundation/history")
t.SetHeader("User-Agent", "irmaserver")
var x string
_ = t.Post("email", &x, s.conf.Email)
}
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
for _, f := range []func(*server.Configuration) error{
s.verifyIrmaConf, s.verifyPrivateKeys, s.verifyRevocation, s.verifyURL, s.verifyEmail,
} {
if err := f(configuration); err != nil {
return err
}
}
return nil
}
func (s *Server) validateRequest(request irma.SessionRequest) error { func (s *Server) validateRequest(request irma.SessionRequest) error {
if _, err := s.conf.IrmaConfiguration.Download(request); err != nil { if _, err := s.conf.IrmaConfiguration.Download(request); err != nil {
return err return err
......
...@@ -14,7 +14,6 @@ import ( ...@@ -14,7 +14,6 @@ import (
"time" "time"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter" prefixed "github.com/x-cray/logrus-prefixed-formatter"
...@@ -22,59 +21,6 @@ import ( ...@@ -22,59 +21,6 @@ import (
var Logger *logrus.Logger = logrus.StandardLogger() var Logger *logrus.Logger = logrus.StandardLogger()
// Configuration contains configuration for the irmaserver library and irmad.
type Configuration struct {
// irma_configuration. If not given, this will be popupated using SchemesPath.
IrmaConfiguration *irma.Configuration `json:"-"`
// Path to IRMA schemes to parse into IrmaConfiguration (only used if IrmaConfiguration == nil).
// If left empty, default value is taken using DefaultSchemesPath().
// If an empty folder is specified, default schemes (irma-demo and pbdf) are downloaded into it.
SchemesPath string `json:"schemes_path" mapstructure:"schemes_path"`
// If specified, schemes found here are copied into SchemesPath (only used if IrmaConfiguration == nil)
SchemesAssetsPath string `json:"schemes_assets_path" mapstructure:"schemes_assets_path"`
// Disable scheme updating
DisableSchemesUpdate bool `json:"disable_schemes_update" mapstructure:"disable_schemes_update"`
// Update all schemes every x minutes (default value 0 means 60) (use DisableSchemesUpdate to disable)
SchemesUpdateInterval int `json:"schemes_update" mapstructure:"schemes_update"`
// Path to issuer private keys to parse
IssuerPrivateKeysPath string `json:"privkeys" mapstructure:"privkeys"`
// Issuer private keys
IssuerPrivateKeys map[irma.IssuerIdentifier]*gabi.PrivateKey `json:"-"`
// URL at which the IRMA app can reach this server during sessions
URL string `json:"url" mapstructure:"url"`
// Required to be set to true if URL does not begin with https:// in production mode.
// In this case, the server would communicate with IRMA apps over plain HTTP. You must otherwise
// ensure (using eg a reverse proxy with TLS enabled) that the attributes are protected in transit.
DisableTLS bool `json:"no_tls" mapstructure:"no_tls"`
// (Optional) email address of server admin, for incidental notifications such as breaking API changes
// See https://github.com/privacybydesign/irmago/tree/master/server#specifying-an-email-address
// for more information
Email string `json:"email" mapstructure:"email"`
// Enable server sent events for status updates (experimental; tends to hang when a reverse proxy is used)
EnableSSE bool `json:"enable_sse" mapstructure:"enable_sse"`
// Logging verbosity level: 0 is normal, 1 includes DEBUG level, 2 includes TRACE level
Verbose int `json:"verbose" mapstructure:"verbose"`
// Don't log anything at all
Quiet bool `json:"quiet" mapstructure:"quiet"`
// Output structured log in JSON format
LogJSON bool `json:"log_json" mapstructure:"log_json"`
// Custom logger instance. If specified, Verbose, Quiet and LogJSON are ignored.
Logger *logrus.Logger `json:"-"`
// Path at which to store revocation databases
RevocationPath string `json:"revocation_path" mapstructure:"revocation_path"`
// Credentials types for which revocation database should be hosted
RevocationServers map[irma.CredentialTypeIdentifier]RevocationServer `json:"revocation_servers" mapstructure:"revocation_servers"`
// Production mode: enables safer and stricter defaults and config checking
Production bool `json:"production" mapstructure:"production"`
}
type RevocationServer struct {
PostURLs []string `json:"post_urls" mapstructure:"post_urls"`
}
type SessionPackage struct { type SessionPackage struct {
SessionPtr *irma.Qr `json:"sessionPtr"` SessionPtr *irma.Qr `json:"sessionPtr"`
Token string `json:"token"` Token string `json:"token"`
...@@ -125,31 +71,6 @@ func (r *SessionResult) Legacy() *LegacySessionResult { ...@@ -125,31 +71,6 @@ func (r *SessionResult) Legacy() *LegacySessionResult {
return &LegacySessionResult{r.Token, r.Status, r.Type, r.ProofStatus, disclosed, r.Signature, r.Err} return &LegacySessionResult{r.Token, r.Status, r.Type, r.ProofStatus, disclosed, r.Signature, r.Err}
} }
func (conf *Configuration) PrivateKey(id irma.IssuerIdentifier) (sk *gabi.PrivateKey, err error) {
sk = conf.IssuerPrivateKeys[id]
if sk == nil {
if sk, err = conf.IrmaConfiguration.PrivateKey(id); err != nil {
return nil, err
}
}
return sk, nil
}
func (conf *Configuration) HavePrivateKeys() (bool, error) {
var err error
var sk *gabi.PrivateKey
for id := range conf.IrmaConfiguration.Issuers {
sk, err = conf.PrivateKey(id)
if err != nil {
return false, err
}
if sk != nil {
return true, nil
}
}
return false, nil
}
func (status Status) Finished() bool { func (status Status) Finished() bool {
return status == StatusDone || status == StatusCancelled || status == StatusTimeout return status == StatusDone || status == StatusCancelled || status == StatusTimeout
} }
......
package server
import (
"io/ioutil"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs"
"github.com/sirupsen/logrus"
)
// Configuration contains configuration for the irmaserver library and irmad.
type Configuration struct {
// irma_configuration. If not given, this will be popupated using SchemesPath.
IrmaConfiguration *irma.Configuration `json:"-"`
// Path to IRMA schemes to parse into IrmaConfiguration (only used if IrmaConfiguration == nil).
// If left empty, default value is taken using DefaultSchemesPath().
// If an empty folder is specified, default schemes (irma-demo and pbdf) are downloaded into it.
SchemesPath string `json:"schemes_path" mapstructure:"schemes_path"`
// If specified, schemes found here are copied into SchemesPath (only used if IrmaConfiguration == nil)
SchemesAssetsPath string `json:"schemes_assets_path" mapstructure:"schemes_assets_path"`
// Disable scheme updating
DisableSchemesUpdate bool `json:"disable_schemes_update" mapstructure:"disable_schemes_update"`
// Update all schemes every x minutes (default value 0 means 60) (use DisableSchemesUpdate to disable)
SchemesUpdateInterval int `json:"schemes_update" mapstructure:"schemes_update"`
// Path to issuer private keys to parse
IssuerPrivateKeysPath string `json:"privkeys" mapstructure:"privkeys"`
// Issuer private keys
IssuerPrivateKeys map[irma.IssuerIdentifier]*gabi.PrivateKey `json:"-"`
// URL at which the IRMA app can reach this server during sessions
URL string `json:"url" mapstructure:"url"`
// Required to be set to true if URL does not begin with https:// in production mode.
// In this case, the server would communicate with IRMA apps over plain HTTP. You must otherwise
// ensure (using eg a reverse proxy with TLS enabled) that the attributes are protected in transit.
DisableTLS bool `json:"disable_tls" mapstructure:"disable_tls"`
// (Optional) email address of server admin, for incidental notifications such as breaking API changes
// See https://github.com/privacybydesign/irmago/tree/master/server#specifying-an-email-address
// for more information
Email string `json:"email" mapstructure:"email"`
// Enable server sent events for status updates (experimental; tends to hang when a reverse proxy is used)
EnableSSE bool `json:"enable_sse" mapstructure:"enable_sse"`
// Logging verbosity level: 0 is normal, 1 includes DEBUG level, 2 includes TRACE level
Verbose int `json:"verbose" mapstructure:"verbose"`
// Don't log anything at all
Quiet bool `json:"quiet" mapstructure:"quiet"`
// Output structured log in JSON format
LogJSON bool `json:"log_json" mapstructure:"log_json"`
// Custom logger instance. If specified, Verbose, Quiet and LogJSON are ignored.
Logger *logrus.Logger `json:"-"`
// Path at which to store revocation databases
RevocationPath string `json:"revocation_path" mapstructure:"revocation_path"`
// Credentials types for which revocation database should be hosted
RevocationServers map[irma.CredentialTypeIdentifier]RevocationServer `json:"revocation_servers" mapstructure:"revocation_servers"`
// Production mode: enables safer and stricter defaults and config checking
Production bool `json:"production" mapstructure:"production"`
}
type RevocationServer struct {
PostURLs []string `json:"post_urls" mapstructure:"post_urls"`
}
// Check ensures that the Configuration is loaded, usable and free of errors.
func (conf *Configuration) Check() error {
if conf.Logger == nil {
conf.Logger = NewLogger(conf.Verbose, conf.Quiet, conf.LogJSON)
}
Logger = conf.Logger
irma.Logger = conf.Logger
// loop to avoid repetetive err != nil line triplets
for _, f := range []func() error{
conf.verifyIrmaConf, conf.verifyPrivateKeys, conf.verifyRevocation, conf.verifyURL, conf.verifyEmail,
} {
if err := f(); err != nil {
return err
}
}
return nil
}
func (conf *Configuration) PrivateKey(id irma.IssuerIdentifier) (sk *gabi.PrivateKey, err error) {
sk = conf.IssuerPrivateKeys[id]
if sk == nil {
if sk, err = conf.IrmaConfiguration.PrivateKey(id); err != nil {
return nil, err
}
}
return sk, nil
}
func (conf *Configuration) HavePrivateKeys() (bool, error) {
var err error
var sk *gabi.PrivateKey
for id := range conf.IrmaConfiguration.Issuers {
sk, err = conf.PrivateKey(id)
if err != nil {
return false, err
}
if sk != nil {
return true, nil
}
}
return false, nil
}
// helpers
func (conf *Configuration) verifyIrmaConf() error {
if conf.IrmaConfiguration == nil {
var (
err error
exists bool
)
if conf.SchemesPath == "" {
conf.SchemesPath = irma.DefaultSchemesPath() // Returns an existing path
}
if exists, err = fs.PathExists(conf.SchemesPath); err != nil {
return LogError(err)
}
if !exists {
return LogError(errors.Errorf("Nonexisting schemes_path provided: %s", conf.SchemesPath))
}
conf.Logger.WithField("schemes_path", conf.SchemesPath).Info("Determined schemes path")
if conf.SchemesAssetsPath == "" {
conf.IrmaConfiguration, err = irma.NewConfiguration(conf.SchemesPath)
} else {
conf.IrmaConfiguration, err = irma.NewConfigurationFromAssets(conf.SchemesPath, conf.SchemesAssetsPath)
}
if err != nil {
return LogError(err)
}
if err = conf.IrmaConfiguration.ParseFolder(); err != nil {
return LogError(err)
}
if err = fs.EnsureDirectoryExists(conf.RevocationPath); err != nil {
return LogError(err)
}
conf.IrmaConfiguration.RevocationPath = conf.RevocationPath
}
if len(conf.IrmaConfiguration.SchemeManagers) == 0 {
conf.Logger.Infof("No schemes found in %s, downloading default (irma-demo and pbdf)", conf.SchemesPath)
if err := conf.IrmaConfiguration.DownloadDefaultSchemes(); err != nil {
return LogError(err)
}
}
if conf.SchemesUpdateInterval == 0 {
conf.SchemesUpdateInterval = 60
}
if !conf.DisableSchemesUpdate {
conf.IrmaConfiguration.AutoUpdateSchemes(uint(conf.SchemesUpdateInterval))
}
return nil
}
func (conf *Configuration) verifyPrivateKeys() error {
if conf.IssuerPrivateKeys == nil {
conf.IssuerPrivateKeys = make(map[irma.IssuerIdentifier]*gabi.PrivateKey)
}
if conf.IssuerPrivateKeysPath != "" {
files, err := ioutil.ReadDir(conf.IssuerPrivateKeysPath)
if err != nil {
return LogError(err)
}
for _, file := range files {
filename := file.Name()
if filepath.Ext(filename) != ".xml" || filename[0] == '.' || strings.Count(filename, ".") != 2 {
conf.Logger.WithField("file", filename).Infof("Skipping non-private key file encountered in private keys path")
continue
}
issid := irma.NewIssuerIdentifier(strings.TrimSuffix(filename, filepath.Ext(filename))) // strip .xml
if _, ok := conf.IrmaConfiguration.Issuers[issid]; !ok {
return LogError(errors.Errorf("Private key %s belongs to an unknown issuer", filename))
}
sk, err := gabi.NewPrivateKeyFromFile(filepath.Join(conf.IssuerPrivateKeysPath, filename))
if err != nil {
return LogError(err)
}
conf.IssuerPrivateKeys[issid] = sk
}
}
for issid, sk := range conf.IssuerPrivateKeys {
pk, err := conf.IrmaConfiguration.PublicKey(issid, int(sk.Counter))
if err != nil {
return LogError(err)
}
if pk == nil {
return LogError(errors.Errorf("Missing public key belonging to private key %s-%d", issid.String(), sk.Counter))
}
if new(big.Int).Mul(sk.P, sk.Q).Cmp(pk.N) != 0 {
return LogError(errors.Errorf("Private key %s-%d does not belong to corresponding public key", issid.String(), sk.Counter))
}
}
return nil
}
func (conf *Configuration) verifyRevocation() error {
for credid, settings := range conf.RevocationServers {
if _, known := conf.IrmaConfiguration.CredentialTypes[credid]; !known {
return LogError(errors.Errorf("unknown credential type %s in revocation settings", credid))
}
db, err := conf.IrmaConfiguration.RevocationDB(credid)
if err != nil {
return LogError(err)
}
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 {
conf.Logger.Warn("error sending revocation update", err)
}
}
})
}
return nil
}
func (conf *Configuration) verifyURL() error {
if conf.URL != "" {
if !strings.HasSuffix(conf.URL, "/") {
conf.URL = conf.URL + "/"
}
if !strings.HasPrefix(conf.URL, "https://") {
if !conf.Production || conf.DisableTLS {
conf.DisableTLS = true
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.", conf.URL)
} else {
return LogError(errors.Errorf("Running without TLS in production mode is unsafe without a reverse proxy. " +
"Either use a https:// URL or explicitly disable TLS."))
}
}
} else {
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")
}
return nil
}
func (conf *Configuration) verifyEmail() error {
if conf.Email != "" {
// Very basic sanity checks
if !strings.Contains(conf.Email, "@") || strings.Contains(conf.Email, "\n") {
return LogError(errors.New("Invalid email address specified"))
}
t := irma.NewHTTPTransport("https://metrics.privacybydesign.foundation/history")
t.SetHeader("User-Agent", "irmaserver")
var x string
_ = t.Post("email", &x, conf.Email)
}
return nil
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment