Commit 6e9d0360 authored by David Venhoek's avatar David Venhoek Committed by Sietse Ringers
Browse files

Added initial (very rough) version of keyshare core server.

parent a800ad0e
......@@ -48,6 +48,9 @@ type Configuration struct {
// (i.e., invalid signature, parsing error), and the problem that occurred when parsing them
DisabledSchemeManagers map[SchemeManagerIdentifier]*SchemeManagerError
// Listeners for configuration changes from initialization and updating of the schemes
UpdateListeners []ConfigurationListener
// Path to the irma_configuration folder that this instance represents
Path string
PrivateKeys PrivateKeyRing
......@@ -64,6 +67,9 @@ type Configuration struct {
readOnly bool
}
// ConfigurationListeners are the interface provided to react to changes in schemes.
type ConfigurationListener func(conf *Configuration)
type UnknownIdentifierError struct {
ErrorType
Missing *IrmaIdentifierSet
......@@ -179,6 +185,7 @@ func (conf *Configuration) ParseFolder() (err error) {
}
conf.initialized = true
conf.CallListeners()
if mgrerr != nil {
return mgrerr
}
......@@ -780,6 +787,8 @@ func (conf *Configuration) join(other *Configuration) {
for key, val := range other.publicKeys {
conf.publicKeys[key] = val
}
conf.CallListeners()
}
func (e *UnknownIdentifierError) Error() string {
......@@ -852,3 +861,9 @@ func firstExistingPath(paths []string) string {
}
return ""
}
func (conf *Configuration) CallListeners() {
for _, listener := range conf.UpdateListeners {
listener(conf)
}
}
package keyshareServerCore
import (
"encoding/binary"
"io/ioutil"
"os"
"strings"
"github.com/privacybydesign/irmago/internal/common"
"github.com/privacybydesign/irmago/keyshareCore"
"github.com/dgrijalva/jwt-go"
"github.com/go-errors/errors"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server"
"github.com/sirupsen/logrus"
)
// Configuration contains configuration for the irmaserver library and irmad.
type Configuration struct {
// Irma server configuration. If not given, this will be populated using information here
ServerConfiguration *server.Configuration `json:"-"`
// Path to IRMA schemes to parse into server configuration (only used if ServerConfiguration == 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 ServerConfiguration == nil)
SchemesAssetsPath string `json:"schemes_assets_path" mapstructure:"schemes_assets_path"`
// Disable scheme updating (used only if ServerConfiguration == nil)
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)
// (used only if ServerConfiguration == nil)
SchemesUpdateInterval int `json:"schemes_update" mapstructure:"schemes_update"`
// Path to issuer private keys to parse
IssuerPrivateKeysPath string `json:"privkeys" mapstructure:"privkeys"`
// URL at which the IRMA app can reach this keyshare 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"`
// Configuration of secure Core
// Private key used to sign JWTs with
JwtKeyId int `json:"jwt_key_id" mapstructure:"jwt_key_id"`
JwtPrivateKey string `json:"jwt_privkey" mapstructure:"jwt_privkey"`
JwtPrivateKeyFile string `json:"jwt_privkey_file" mapstructure:"jwt_privkey_file"`
// Decryption keys used for keyshare packets
StorageFallbackKeyFiles []string `json:"storage_fallback_key_files" mapstructure:"storage_fallback_key_files"`
StoragePrimaryKeyFile string `json:"storage_primary_key_file" mapstructure:"storage_primary_key_file"`
// Keyshare credential to issue during registration
KeyshareCredential string
KeyshareAttribute string
// 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:"-"`
// Production mode: enables safer and stricter defaults and config checking
Production bool `json:"production" mapstructure:"production"`
}
func readAESKey(filename string) (uint32, keyshareCore.AesKey, error) {
keyFile, err := os.Open(filename)
if err != nil {
return 0, keyshareCore.AesKey{}, err
}
defer keyFile.Close()
keyData, err := ioutil.ReadAll(keyFile)
if err != nil {
return 0, keyshareCore.AesKey{}, err
}
if len(keyData) != 32+4 {
return 0, keyshareCore.AesKey{}, errors.New("Invalid aes key")
}
var key [32]byte
copy(key[:], keyData[4:36])
return binary.LittleEndian.Uint32(keyData[0:4]), key, nil
}
// Process a passed configuration to ensure all field values are valid and initialized
// as required by the rest of this keyshare server component.
func processConfiguration(conf *Configuration) (*keyshareCore.KeyshareCore, error) {
// Setup log
if conf.Logger == nil {
conf.Logger = server.NewLogger(conf.Verbose, conf.Quiet, conf.LogJSON)
}
server.Logger = conf.Logger
irma.Logger = conf.Logger
// Setup server configuration if needed
if conf.ServerConfiguration == nil {
conf.ServerConfiguration = &server.Configuration{
SchemesPath: conf.SchemesPath,
SchemesAssetsPath: conf.SchemesAssetsPath,
DisableSchemesUpdate: conf.DisableSchemesUpdate,
SchemesUpdateInterval: conf.SchemesUpdateInterval,
IssuerPrivateKeysPath: conf.IssuerPrivateKeysPath,
DisableTLS: conf.DisableTLS,
Logger: conf.Logger,
}
}
// Force loggers to match (TODO: reevaluate once logging is reworked in irma server)
conf.ServerConfiguration.Logger = conf.Logger
// Force production status to match
conf.ServerConfiguration.Production = conf.Production
// Load configuration (because server setup needs this to be in place)
if conf.ServerConfiguration.IrmaConfiguration == nil {
var (
err error
exists bool
)
if conf.ServerConfiguration.SchemesPath == "" {
conf.ServerConfiguration.SchemesPath = irma.DefaultSchemesPath() // Returns an existing path
}
if exists, err = common.PathExists(conf.ServerConfiguration.SchemesPath); err != nil {
return nil, server.LogError(err)
}
if !exists {
return nil, server.LogError(errors.Errorf("Nonexisting schemes_path provided: %s", conf.ServerConfiguration.SchemesPath))
}
conf.Logger.WithField("schemes_path", conf.ServerConfiguration.SchemesPath).Info("Determined schemes path")
conf.ServerConfiguration.IrmaConfiguration, err = irma.NewConfiguration(conf.ServerConfiguration.SchemesPath, irma.ConfigurationOptions{
Assets: conf.ServerConfiguration.SchemesAssetsPath,
})
if err != nil {
return nil, server.LogError(err)
}
if err = conf.ServerConfiguration.IrmaConfiguration.ParseFolder(); err != nil {
return nil, server.LogError(err)
}
}
// Setup server urls
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 nil, 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."))
}
}
if conf.ServerConfiguration.URL == "" {
conf.ServerConfiguration.URL = conf.URL + "irma/"
conf.ServerConfiguration.DisableTLS = conf.DisableTLS // ensure matching checks
}
// Parse keyshareCore private keys and create a valid keyshare core
core := keyshareCore.NewKeyshareCore()
if conf.JwtPrivateKey == "" && conf.JwtPrivateKeyFile == "" {
return nil, server.LogError(errors.Errorf("Missing keyshare server jwt key"))
}
keybytes, err := common.ReadKey(conf.JwtPrivateKey, conf.JwtPrivateKeyFile)
if err != nil {
return nil, server.LogError(errors.WrapPrefix(err, "failed to read keyshare server jwt key", 0))
}
jwtPrivateKey, err := jwt.ParseRSAPrivateKeyFromPEM(keybytes)
if err != nil {
return nil, server.LogError(errors.WrapPrefix(err, "failed to read keyshare server jwt key", 0))
}
core.DangerousSetSignKey(jwtPrivateKey)
encId, encKey, err := readAESKey(conf.StoragePrimaryKeyFile)
if err != nil {
return nil, server.LogError(errors.WrapPrefix(err, "failed to load primary storage key", 0))
}
core.DangerousSetAESEncryptionKey(encId, encKey)
for _, keyFile := range conf.StorageFallbackKeyFiles {
id, key, err := readAESKey(keyFile)
if err != nil {
return nil, server.LogError(errors.WrapPrefix(err, "failed to load fallback key "+keyFile, 0))
}
core.DangerousAddAESKey(id, key)
}
return core, nil
}
package keyshareServerCore
import (
"errors"
"sync"
"github.com/privacybydesign/irmago/keyshareCore"
)
var (
ErrUserAlreadyExists = errors.New("Cannot create user, username already taken")
ErrUserNotFound = errors.New("Could not find specified user")
)
type KeyshareDB interface {
NewUser(user KeyshareUser) error
User(username string) (KeyshareUser, error)
UpdateUser(user KeyshareUser) error
}
type KeyshareUser struct {
Username string
Coredata keyshareCore.EncryptedKeysharePacket
}
type keyshareMemoryDB struct {
lock sync.Mutex
users map[string]keyshareCore.EncryptedKeysharePacket
}
func NewMemoryDatabase() KeyshareDB {
return &keyshareMemoryDB{users: map[string]keyshareCore.EncryptedKeysharePacket{}}
}
func (db *keyshareMemoryDB) User(username string) (KeyshareUser, error) {
// Ensure access to database is single-threaded
db.lock.Lock()
defer db.lock.Unlock()
// Check and fetch user data
data, ok := db.users[username]
if !ok {
return KeyshareUser{}, ErrUserNotFound
}
return KeyshareUser{Username: username, Coredata: data}, nil
}
func (db *keyshareMemoryDB) NewUser(user KeyshareUser) error {
// Ensure access to database is single-threaded
db.lock.Lock()
defer db.lock.Unlock()
// Check and insert user
_, exists := db.users[user.Username]
if exists {
return ErrUserAlreadyExists
}
db.users[user.Username] = user.Coredata
return nil
}
func (db *keyshareMemoryDB) UpdateUser(user KeyshareUser) error {
// Ensure access to database is single-threaded
db.lock.Lock()
defer db.lock.Unlock()
// Check and update user.
_, exists := db.users[user.Username]
if exists {
return ErrUserNotFound
}
db.users[user.Username] = user.Coredata
return nil
}
package keyshareServerCore
import (
"github.com/privacybydesign/gabi"
)
type keyshareEnrollment struct {
Username string `json:"username"`
Pin string `json:"pin"`
Email *string `json:"email"`
Language string `json:"language"`
}
type keyshareChangepin struct {
Username string `json:"id"`
OldPin string `json:"oldpin"`
NewPin string `json:"newpin"`
}
type keyshareAuthorization struct {
Status string `json:"status"`
Candidates []string `json:"candidates"`
}
type keysharePinMessage struct {
Username string `json:"id"`
Pin string `json:"pin"`
}
type keysharePinStatus struct {
Status string `json:"status"`
Message string `json:"message"`
}
type proofPCommitmentMap struct {
Commitments map[string]*gabi.ProofPCommitment `json:"c"`
}
package keyshareServerCore
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
irma "github.com/privacybydesign/irmago"
"github.com/sirupsen/logrus"
"github.com/privacybydesign/irmago/keyshareCore"
"github.com/privacybydesign/irmago/server"
"github.com/privacybydesign/irmago/server/irmaserver"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
)
type SessionData struct {
LastKeyid irma.PublicKeyIdentifier
LastCommitID uint64
}
type Server struct {
conf *Configuration
core *keyshareCore.KeyshareCore
sessionserver *irmaserver.Server
db KeyshareDB
sessions map[string]*SessionData
sessionLock sync.Mutex
}
func New(conf *Configuration) (*Server, error) {
var err error
s := &Server{
conf: conf,
sessions: map[string]*SessionData{},
}
// Do initial processing of configuration and create keyshare core
s.core, err = processConfiguration(conf)
if err != nil {
return nil, err
}
// Load neccessary idemix keys into core, and ensure that future updates
// to them are processed
s.LoadIdemixKeys(conf.ServerConfiguration.IrmaConfiguration)
conf.ServerConfiguration.IrmaConfiguration.UpdateListeners = append(
conf.ServerConfiguration.IrmaConfiguration.UpdateListeners,
s.LoadIdemixKeys)
// Setup irma session server
s.sessionserver, err = irmaserver.New(conf.ServerConfiguration)
if err != nil {
return nil, err
}
// Setup DB (TODO: make configurable)
s.db = NewMemoryDatabase()
return s, nil
}
func (s *Server) Handler() http.Handler {
router := chi.NewRouter()
router.Use(middleware.Logger)
router.Post("/api/v1/client/register", s.handleRegister)
router.Post("/api/v1/users/isAuthorized", s.handleValidate)
router.Post("/api/v1/users/verify/pin", s.handleVerifyPin)
router.Post("/api/v1/prove/getCommitments", s.handleCommitments)
router.Post("/api/v1/prove/getResponse", s.handleResponse)
router.Mount("/irma/", s.sessionserver.HandlerFunc())
return router
}
func (s *Server) LoadIdemixKeys(conf *irma.Configuration) {
fmt.Println("load called")
for _, issuer := range conf.Issuers {
keyIds, err := conf.PublicKeyIndices(issuer.Identifier())
if err != nil {
s.conf.Logger.WithFields(logrus.Fields{"issuer": issuer, "error": err}).Warn("Could not find key ids for issuer")
continue
}
for _, id := range keyIds {
key, err := conf.PublicKey(issuer.Identifier(), id)
if err != nil {
s.conf.Logger.WithFields(logrus.Fields{"keyid": id, "error": err}).Warn("Could not fetch public key for issuer")
continue
}
s.core.DangerousAddTrustedPublicKey(irma.PublicKeyIdentifier{Issuer: issuer.Identifier(), Counter: uint(id)}, key)
}
}
}
func (s *Server) handleCommitments(w http.ResponseWriter, r *http.Request) {
// Read keys
body, err := ioutil.ReadAll(r.Body)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not read request body")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
fmt.Println(string(body))
var keys []irma.PublicKeyIdentifier
err = json.Unmarshal(body, &keys)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not parse request body")
s.conf.Logger.WithField("body", body).Debug("Malformed request data")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
if len(keys) == 0 {
s.conf.Logger.Info("Malformed request: no keys over which to commit specified")
server.WriteError(w, server.ErrorInvalidRequest, "No key specified")
return
}
// Extract username and authorization from request
username := r.Header.Get("X-IRMA-Keyshare-Username")
authorization := r.Header.Get("Authorization")
user, err := s.db.User(username)
if err != nil {
s.conf.Logger.WithFields(logrus.Fields{"username": username, "error": err}).Warn("User not found in db")
server.WriteError(w, server.ErrorUserNotRegistered, err.Error())
return
}
// TODO: block check
commitments, commitId, err := s.core.GenerateCommitments(user.Coredata, authorization, keys)
if err != nil {
s.conf.Logger.WithField("error", err).Warn("Could not generate commitments for request")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
// Prepare output message format
mappedCommitments := map[string]*gabi.ProofPCommitment{}
for i, keyid := range keys {
keyidV, err := keyid.MarshalText()
if err != nil {
s.conf.Logger.WithFields(logrus.Fields{"keyid": keyid, "error": err}).Error("Could not convert key identifier to string")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
mappedCommitments[string(keyidV)] = commitments[i]
}
// Store needed data for later requests.
s.sessionLock.Lock()
if _, ok := s.sessions[username]; !ok {
s.sessions[username] = &SessionData{}
}
s.sessions[username].LastCommitID = commitId
s.sessions[username].LastKeyid = keys[0]
s.sessionLock.Unlock()
// And send response
server.WriteJson(w, proofPCommitmentMap{Commitments: mappedCommitments})
}
func (s *Server) handleResponse(w http.ResponseWriter, r *http.Request) {
// Read challenge
body, err := ioutil.ReadAll(r.Body)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not read request body")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
challenge := new(big.Int)
err = json.Unmarshal(body, challenge)
if err != nil {
s.conf.Logger.Info("Malformed request: could not parse challenge")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
// Extract username and authorization from request
username := r.Header.Get("X-IRMA-Keyshare-Username")
authorization := r.Header.Get("Authorization")
// Fetch user
user, err := s.db.User(username)
if err != nil {
s.conf.Logger.WithFields(logrus.Fields{"username": username, "error": err}).Warn("Could not find user in db")
server.WriteError(w, server.ErrorUserNotRegistered, err.Error())
return
}
// verify access (avoids leaking information to unauthorized callers)
err = s.core.ValidateJWT(user.Coredata, authorization)
if err != nil {
s.conf.Logger.WithField("error", err).Warn("Could not generate keyshare response")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
// Get data from session
s.sessionLock.Lock()
sessionData, ok := s.sessions[username]
s.sessionLock.Unlock()
if !ok {
s.conf.Logger.Warn("Request for response without previous call to get commitments")
server.WriteError(w, server.ErrorInvalidRequest, "Missing previous call to getCommitments")
return
}
proofResponse, err := s.core.GenerateResponse(user.Coredata, authorization, sessionData.LastCommitID, challenge, sessionData.LastKeyid)
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not generate response for request")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
server.WriteString(w, proofResponse)
}
func (s *Server) handleValidate(w http.ResponseWriter, r *http.Request) {
// Extract username and authorization from request
username := r.Header.Get("X-IRMA-Keyshare-Username")
authorization := r.Header.Get("Authorization")
user, err := s.db.User(username)
if err != nil {
s.conf.Logger.WithFields(logrus.Fields{"username": username, "error": err}).Warn("Could not find user in db")
server.WriteError(w, server.ErrorUserNotRegistered, err.Error())
return
}
// TODO: Block check
err = s.core.ValidateJWT(user.Coredata, authorization)
if err != nil {
server.WriteJson(w, &keyshareAuthorization{Status: "expired", Candidates: []string{"pin"}})
} else {
server.WriteJson(w, &keyshareAuthorization{Status: "authorized", Candidates: []string{"pin"}})
}
}
func (s *Server) handleVerifyPin(w http.ResponseWriter, r *http.Request) {
// Extract request
body, err := ioutil.ReadAll(r.Body)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not read request body")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
var msg keysharePinMessage