Commit 5c5891fb authored by Sietse Ringers's avatar Sietse Ringers
Browse files

feat: prevent private keys from being cached in irma server

parent ffa1dd89
......@@ -306,8 +306,7 @@ func TestIrmaServerPrivateKeysFolder(t *testing.T) {
credid := irma.NewCredentialTypeIdentifier("irma-demo.RU.studentCard")
require.NotZero(t, len(irmaServerConfiguration.IrmaConfiguration.PrivateKeys))
sk, err := irmaServerConfiguration.IrmaConfiguration.PrivateKeyLatest(credid.IssuerIdentifier())
sk, err := irmaServerConfiguration.IrmaConfiguration.PrivateKeys.Latest(credid.IssuerIdentifier())
require.NoError(t, err)
require.NotNil(t, sk)
......@@ -328,8 +327,7 @@ func TestIrmaServerPrivateKeysFolder(t *testing.T) {
AttributeTypes: map[irma.AttributeTypeIdentifier]struct{}{},
}, downloaded)
require.NotZero(t, len(irmaServerConfiguration.IrmaConfiguration.PrivateKeys))
sk, err = irmaServerConfiguration.IrmaConfiguration.PrivateKeyLatest(credid.IssuerIdentifier())
sk, err = irmaServerConfiguration.IrmaConfiguration.PrivateKeys.Latest(credid.IssuerIdentifier())
require.NoError(t, err)
require.NotNil(t, sk)
}
......
......@@ -32,7 +32,6 @@ import (
"github.com/go-errors/errors"
"github.com/jasonlvhit/gocron"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/signed"
"github.com/privacybydesign/irmago/internal/common"
"github.com/sirupsen/logrus"
......@@ -46,9 +45,7 @@ type Configuration struct {
CredentialTypes map[CredentialTypeIdentifier]*CredentialType
AttributeTypes map[AttributeTypeIdentifier]*AttributeType
// Issuer private keys. If set (after calling ParseFolder()), will use these keys
// instead of keys in irma_configuration/$issuer/PrivateKeys.
PrivateKeys map[IssuerIdentifier]map[uint]*gabi.PrivateKey
PrivateKeys PrivateKeyRing
Revocation *RevocationStorage `json:"-"`
......@@ -162,7 +159,7 @@ func (conf *Configuration) clear() {
conf.publicKeys = make(map[IssuerIdentifier]map[uint]*gabi.PublicKey)
conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
if conf.PrivateKeys == nil { // keep if already populated
conf.PrivateKeys = make(map[IssuerIdentifier]map[uint]*gabi.PrivateKey)
conf.PrivateKeys = &privateKeyRingMerge{}
}
}
......@@ -211,6 +208,14 @@ func (conf *Configuration) ParseFolder() (err error) {
return
}
if len(conf.PrivateKeys.(*privateKeyRingMerge).rings) == 0 {
ring, err := newPrivateKeyRingScheme(conf.Path, conf)
if err != nil {
return err
}
conf.PrivateKeys.(*privateKeyRingMerge).Add(ring)
}
if conf.Revocation == nil {
conf.Scheduler = gocron.NewScheduler()
conf.Scheduler.Start()
......@@ -331,42 +336,12 @@ func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeM
return
}
// PrivateKey returns the specified private key of the specified issuer if present; an error otherwise.
func (conf *Configuration) PrivateKey(id IssuerIdentifier, counter uint) (*gabi.PrivateKey, error) {
if _, haveIssuer := conf.PrivateKeys[id]; haveIssuer {
if sk := conf.PrivateKeys[id][counter]; sk != nil {
return sk, nil
}
}
path := fmt.Sprintf(privkeyPattern, conf.Path, id.SchemeManagerIdentifier().Name(), id.Name())
file := strings.Replace(path, "*", strconv.FormatUint(uint64(counter), 10), 1)
sk, err := gabi.NewPrivateKeyFromFile(file)
if err != nil {
return nil, err
}
if sk.Counter != counter {
return nil, errors.Errorf("Private key %s of issuer %s has wrong <Counter>", file, id.String())
}
if conf.PrivateKeys[id] == nil {
conf.PrivateKeys[id] = make(map[uint]*gabi.PrivateKey)
}
conf.PrivateKeys[id][counter] = sk
return sk, nil
}
// PrivateKeyLatest returns the latest private key of the specified issuer.
func (conf *Configuration) PrivateKeyLatest(id IssuerIdentifier) (*gabi.PrivateKey, error) {
indices, err := conf.PrivateKeyIndices(id)
if err != nil {
return nil, err
}
if len(indices) == 0 {
return nil, errors.New("no private keys found")
func (conf *Configuration) AddPrivateKeyRing(ring PrivateKeyRing) error {
if err := validatePrivateKeyRing(ring, conf); err != nil {
return err
}
return conf.PrivateKey(id, indices[len(indices)-1])
conf.PrivateKeys.(*privateKeyRingMerge).Add(ring)
return nil
}
// PublicKey returns the specified public key, or nil if not present in the Configuration.
......@@ -550,40 +525,12 @@ func sorter(ints []uint) func(i, j int) bool {
return func(i, j int) bool { return ints[i] < ints[j] }
}
// unionset returns the concatenation of a and b, without duplicates, and sorted.
func unionset(a, b []uint) []uint {
m := map[uint]struct{}{}
for _, c := range [][]uint{a, b} {
for _, i := range c {
m[i] = struct{}{}
}
}
var ints []uint
for i := range m {
ints = append(ints, i)
}
sort.Slice(ints, sorter(ints))
return ints
}
func (conf *Configuration) PrivateKeyIndices(issuerid IssuerIdentifier) (i []uint, err error) {
filekeys, err := conf.matchKeyPattern(issuerid, privkeyPattern)
if err != nil {
return nil, err
}
var mapkeys []uint
for _, sk := range conf.PrivateKeys[issuerid] {
mapkeys = append(mapkeys, sk.Counter)
}
return unionset(filekeys, mapkeys), nil
}
func (conf *Configuration) PublicKeyIndices(issuerid IssuerIdentifier) (i []uint, err error) {
return conf.matchKeyPattern(issuerid, pubkeyPattern)
return matchKeyPattern(conf.Path, issuerid, pubkeyPattern)
}
func (conf *Configuration) matchKeyPattern(issuerid IssuerIdentifier, pattern string) (ints []uint, err error) {
pkpath := fmt.Sprintf(pattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
func matchKeyPattern(path string, issuerid IssuerIdentifier, pattern string) (ints []uint, err error) {
pkpath := fmt.Sprintf(pattern, path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
files, err := filepath.Glob(pkpath)
if err != nil {
return
......@@ -1527,34 +1474,6 @@ func (conf *Configuration) ValidateKeys() error {
}
}
// Check private keys if any
indices, err = conf.PrivateKeyIndices(issuerid)
if err != nil {
return err
}
for _, i := range indices {
sk, err := conf.PrivateKey(issuerid, i)
if err != nil {
return err
}
if sk.Counter != i {
return errors.Errorf("Private key %d of issuer %s has wrong <Counter>", i, issuerid.String())
}
pk, err := conf.PublicKey(issuerid, i)
if err != nil {
return err
}
if pk == nil {
return errors.Errorf("Private key %d of issuer %s has no corresponding public key", i, issuerid.String())
}
if new(big.Int).Mul(sk.P, sk.Q).Cmp(pk.N) != 0 {
return errors.Errorf("Private key %d of issuer %s does not belong to corresponding public key", i, issuerid.String())
}
if sk.RevocationSupported() != pk.RevocationSupported() {
return errors.Errorf("revocation support of private key %d of issuer %s is not consistent with corresponding public key", i, issuerid.String())
}
}
// Check that the current public key supports enough attributes for all credential types
// issued by this issuer
for id, typ := range conf.CredentialTypes {
......
package irma
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/go-errors/errors"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
)
type (
// PrivateKeyRing provides access to a set of private keys.
PrivateKeyRing interface {
// Latest returns the private key with the highest counter for the specified issuer, if any,
// or an error.
Latest(id IssuerIdentifier) (*gabi.PrivateKey, error)
// Get returns the specified private key, or an error.
Get(id IssuerIdentifier, counter uint) (*gabi.PrivateKey, error)
// Iterate executes the specified function on each private key of the specified issuer
// present in the ring. The private keys are offered to the function in no particular order,
// and the same key may be offered multiple times. Returns on the first error returned
// by the function.
Iterate(id IssuerIdentifier, f func(sk *gabi.PrivateKey) error) error
}
// PrivateKeyRingFolder represents a folder on disk containing private keys with filenames
// of the form scheme.issuer.xml and scheme.issuer.counter.xml.
PrivateKeyRingFolder struct {
path string
}
// privateKeyRingScheme provides access to private keys present in a scheme.
privateKeyRingScheme struct {
path string
}
// privateKeyRingMerge is a merge of multiple key rings into one, provides access to the
// private keys of all of them.
privateKeyRingMerge struct {
rings []PrivateKeyRing
}
)
func NewPrivateKeyRingFolder(path string, conf *Configuration) (*PrivateKeyRingFolder, error) {
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
ring := &PrivateKeyRingFolder{path}
for _, file := range files {
filename := file.Name()
dotcount := strings.Count(filename, ".")
// filename format may be scheme.issuer.xml or scheme.issuer.counter.xml; skip any other file
if filepath.Ext(filename) != ".xml" || filename[0] == '.' || dotcount < 2 || dotcount > 3 {
Logger.WithField("file", filename).Infof("Skipping non-private key file encountered in private keys path")
continue
}
counter := -1
base := strings.TrimSuffix(filename, filepath.Ext(filename)) // strip .xml
if dotcount == 3 {
index := strings.LastIndex(base, ".")
base = base[:index]
counter, err = strconv.Atoi(base[index+1:])
}
sk, err := ring.readFile(filename)
if err != nil {
return nil, err
}
if counter >= 0 && uint(counter) != sk.Counter {
return nil, errors.Errorf("private key %s has wrong counter %d in filename, should be %d", filename, counter, sk.Counter)
}
if err = validatePrivateKey(NewIssuerIdentifier(base), sk, conf); err != nil {
return nil, err
}
}
return ring, nil
}
func (p *PrivateKeyRingFolder) readFile(filename string) (*gabi.PrivateKey, error) {
return gabi.NewPrivateKeyFromFile(filepath.Join(p.path, filename))
}
func (p *PrivateKeyRingFolder) Get(id IssuerIdentifier, counter uint) (*gabi.PrivateKey, error) {
sk, err := p.readFile(fmt.Sprintf("%s.%d.xml", id.String(), counter))
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
if sk != nil {
return sk, nil
}
sk, err = p.readFile(fmt.Sprintf("%s.xml", id.String()))
if err != nil {
return nil, err
}
if counter != sk.Counter {
return nil, os.ErrNotExist
}
return sk, nil
}
func (p *PrivateKeyRingFolder) Latest(id IssuerIdentifier) (*gabi.PrivateKey, error) {
var sk *gabi.PrivateKey
if err := p.Iterate(id, func(s *gabi.PrivateKey) error {
if sk == nil || s.Counter > sk.Counter {
sk = s
}
return nil
}); err != nil {
return nil, err
}
if sk == nil {
return nil, os.ErrNotExist
}
return sk, nil
}
func (p *PrivateKeyRingFolder) Iterate(id IssuerIdentifier, f func(sk *gabi.PrivateKey) error) error {
files, err := filepath.Glob(filepath.Join(p.path, fmt.Sprintf("%s*", id.String())))
if err != nil {
return err
}
for _, file := range files {
sk, err := p.readFile(filepath.Base(file))
if err != nil {
return err
}
if err = f(sk); err != nil {
return err
}
}
return nil
}
func newPrivateKeyRingScheme(path string, conf *Configuration) (*privateKeyRingScheme, error) {
ring := &privateKeyRingScheme{path}
err := validatePrivateKeyRing(ring, conf)
if err != nil {
return nil, err
}
return ring, nil
}
func (p *privateKeyRingScheme) counters(issuerid IssuerIdentifier) (i []uint, err error) {
return matchKeyPattern(p.path, issuerid, privkeyPattern)
}
func (p *privateKeyRingScheme) Get(id IssuerIdentifier, counter uint) (*gabi.PrivateKey, error) {
path := fmt.Sprintf(privkeyPattern, p.path, id.SchemeManagerIdentifier().Name(), id.Name())
file := strings.Replace(path, "*", strconv.FormatUint(uint64(counter), 10), 1)
sk, err := gabi.NewPrivateKeyFromFile(file)
if err != nil {
return nil, err
}
if sk.Counter != counter {
return nil, errors.Errorf("Private key %s of issuer %s has wrong <Counter>", file, id.String())
}
return sk, nil
}
func (p *privateKeyRingScheme) Latest(id IssuerIdentifier) (*gabi.PrivateKey, error) {
counters, err := p.counters(id)
if err != nil {
return nil, err
}
if len(counters) == 0 {
return nil, os.ErrNotExist
}
return p.Get(id, counters[len(counters)-1])
}
func (p *privateKeyRingScheme) Iterate(id IssuerIdentifier, f func(sk *gabi.PrivateKey) error) error {
indices, err := p.counters(id)
if err != nil {
return err
}
for _, counter := range indices {
sk, err := p.Get(id, counter)
if err != nil {
return err
}
if err = f(sk); err != nil {
return err
}
}
return nil
}
func (p *privateKeyRingMerge) Add(ring PrivateKeyRing) {
p.rings = append(p.rings, ring)
}
func (p *privateKeyRingMerge) Get(id IssuerIdentifier, counter uint) (*gabi.PrivateKey, error) {
for _, ring := range p.rings {
sk, err := ring.Get(id, counter)
if err == nil {
return sk, nil
}
if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
}
return nil, os.ErrNotExist
}
func (p *privateKeyRingMerge) Latest(id IssuerIdentifier) (*gabi.PrivateKey, error) {
var sk *gabi.PrivateKey
for _, ring := range p.rings {
s, err := ring.Latest(id)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
if s != nil && (sk == nil || s.Counter > sk.Counter) {
sk = s
}
}
if sk == nil {
return nil, os.ErrNotExist
}
return sk, nil
}
func (p *privateKeyRingMerge) Iterate(id IssuerIdentifier, f func(sk *gabi.PrivateKey) error) error {
for _, ring := range p.rings {
if err := ring.Iterate(id, f); err != nil {
return err
}
}
return nil
}
func validatePrivateKey(issuerid IssuerIdentifier, sk *gabi.PrivateKey, conf *Configuration) error {
if _, ok := conf.Issuers[issuerid]; !ok {
return errors.Errorf("Private key %d of issuer %s belongs to an unknown issuer", sk.Counter, issuerid.String())
}
pk, err := conf.PublicKey(issuerid, sk.Counter)
if err != nil {
return err
}
if pk == nil {
return errors.Errorf("Private key %d of issuer %s has no corresponding public key", sk.Counter, issuerid.String())
}
if new(big.Int).Mul(sk.P, sk.Q).Cmp(pk.N) != 0 {
return errors.Errorf("Private key %d of issuer %s does not belong to corresponding public key", sk.Counter, issuerid.String())
}
if sk.RevocationSupported() != pk.RevocationSupported() {
return errors.Errorf("revocation support of private key %d of issuer %s is not consistent with corresponding public key", sk.Counter, issuerid.String())
}
return nil
}
func validatePrivateKeyRing(ring PrivateKeyRing, conf *Configuration) error {
for issuerid := range conf.Issuers {
err := ring.Iterate(issuerid, func(sk *gabi.PrivateKey) error {
return validatePrivateKey(issuerid, sk, conf)
})
if err != nil {
return err
}
}
return nil
}
......@@ -977,7 +977,7 @@ func (client RevocationClient) transport(forceHTTPS bool) *HTTPTransport {
}
func (rs RevocationKeys) PrivateKeyLatest(issid IssuerIdentifier) (*revocation.PrivateKey, error) {
sk, err := rs.Conf.PrivateKeyLatest(issid)
sk, err := rs.Conf.PrivateKeys.Latest(issid)
if err != nil {
return nil, err
}
......@@ -992,7 +992,7 @@ func (rs RevocationKeys) PrivateKeyLatest(issid IssuerIdentifier) (*revocation.P
}
func (rs RevocationKeys) PrivateKey(issid IssuerIdentifier, counter uint) (*revocation.PrivateKey, error) {
sk, err := rs.Conf.PrivateKey(issid, counter)
sk, err := rs.Conf.PrivateKeys.Get(issid, counter)
if err != nil {
return nil, err
}
......
......@@ -4,16 +4,13 @@ import (
"crypto/rsa"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/dgrijalva/jwt-go"
"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/common"
"github.com/sirupsen/logrus"
......@@ -35,8 +32,6 @@ type Configuration struct {
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]map[uint]*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.
......@@ -118,7 +113,7 @@ func (conf *Configuration) Check() error {
func (conf *Configuration) HavePrivateKeys() bool {
var err error
for id := range conf.IrmaConfiguration.Issuers {
if _, err = conf.IrmaConfiguration.PrivateKeyLatest(id); err == nil {
if _, err = conf.IrmaConfiguration.PrivateKeys.Latest(id); err == nil {
return true
}
}
......@@ -183,11 +178,6 @@ func (conf *Configuration) verifyIrmaConf() error {
}
}
if len(conf.IssuerPrivateKeys) == 0 {
conf.IssuerPrivateKeys = make(map[irma.IssuerIdentifier]map[uint]*gabi.PrivateKey)
}
conf.IrmaConfiguration.PrivateKeys = conf.IssuerPrivateKeys
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 {
......@@ -205,102 +195,45 @@ func (conf *Configuration) verifyIrmaConf() error {
}
func (conf *Configuration) verifyPrivateKeys() error {
if conf.IssuerPrivateKeys == nil {
conf.IssuerPrivateKeys = make(map[irma.IssuerIdentifier]map[uint]*gabi.PrivateKey)
}
if conf.IssuerPrivateKeysPath != "" {
files, err := ioutil.ReadDir(conf.IssuerPrivateKeysPath)
if err != nil {
return err
}
for _, file := range files {
filename := file.Name()
dotcount := strings.Count(filename, ".")
if filepath.Ext(filename) != ".xml" || filename[0] == '.' || dotcount < 2 || dotcount > 3 {
conf.Logger.WithField("file", filename).Infof("Skipping non-private key file encountered in private keys path")
continue
}
base := strings.TrimSuffix(filename, filepath.Ext(filename))
counter := -1
var err error
if dotcount == 3 {
index := strings.LastIndex(base, ".")
counter, err = strconv.Atoi(base[index+1:])
if err != nil {
return err
}
base = base[:index]
}
issid := irma.NewIssuerIdentifier(base) // strip .xml
if _, ok := conf.IrmaConfiguration.Issuers[issid]; !ok {
return errors.Errorf("Private key %s belongs to an unknown issuer", filename)
}
sk, err := gabi.NewPrivateKeyFromFile(filepath.Join(conf.IssuerPrivateKeysPath, filename))
if err != nil {
return err
}
if counter >= 0 && uint(counter) != sk.Counter {
return errors.Errorf("private key %s has wrong counter %d in filename, should be %d", filename, counter, sk.Counter)
}
if len(conf.IssuerPrivateKeys[issid]) == 0 {
conf.IssuerPrivateKeys[issid] = map[uint]*gabi.PrivateKey{}
}
conf.IssuerPrivateKeys[issid][sk.Counter] = sk
}
if conf.IssuerPrivateKeysPath == "" {
return nil
}
for issid := range conf.IssuerPrivateKeys {
for _, sk := range conf.IssuerPrivateKeys[issid] {
pk, err := conf.IrmaConfiguration.PublicKey(issid, sk.Counter)
if err != nil {
return err
}
if pk == nil {
return 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 errors.Errorf("Private key %s-%d does not belong to corresponding public key", issid.String(), sk.Counter)
}
}
ring, err := irma.NewPrivateKeyRingFolder(conf.IssuerPrivateKeysPath, conf.IrmaConfiguration)
if err != nil {
return err
}
return nil
return conf.IrmaConfiguration.AddPrivateKeyRing(ring)
}
func (conf *Configuration) prepareRevocation(credid irma.CredentialTypeIdentifier) error {
sks, err := conf.IrmaConfiguration.PrivateKeyIndices(credid.IssuerIdentifier())
if err != nil {
return errors.WrapPrefix(err, "failed to load private key indices for revocation", 0)
}
if len(sks) == 0 {
return errors.Errorf("revocation server mode enabled for %s but no private key installed", credid)
}
rev := conf.IrmaConfiguration.Revocation
for _, skcounter := range sks {
isk, err := conf.IrmaConfiguration.PrivateKey(credid.IssuerIdentifier(), skcounter)
if err != nil {
return errors.WrapPrefix(err, fmt.Sprintf("failed to load private key %s-%d for revocation", credid, skcounter), 0)
}
var sk *revocation.PrivateKey
err := conf.IrmaConfiguration.PrivateKeys.Iterate(credid.IssuerIdentifier(), func(isk *gabi.PrivateKey) error {
if !isk.RevocationSupported() {