Commit 33848ed3 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Merge dynamic irma_configuration downloading

parents 235c5e6b 4140d9cb
...@@ -10,16 +10,22 @@ import ( ...@@ -10,16 +10,22 @@ import (
"crypto/sha256" "crypto/sha256"
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/mhe/gabi" "github.com/mhe/gabi"
) )
// ConfigurationStore keeps track of scheme managers, issuers, credential types and public keys. // ConfigurationStore keeps track of scheme managers, issuers, credential types and public keys,
// dezerializing them from an irma_configuration folder, and downloads and saves new ones on demand.
type ConfigurationStore struct { type ConfigurationStore struct {
SchemeManagers map[SchemeManagerIdentifier]*SchemeManager SchemeManagers map[SchemeManagerIdentifier]*SchemeManager
Issuers map[IssuerIdentifier]*Issuer Issuers map[IssuerIdentifier]*Issuer
CredentialTypes map[CredentialTypeIdentifier]*CredentialType CredentialTypes map[CredentialTypeIdentifier]*CredentialType
publicKeys map[IssuerIdentifier][]*gabi.PublicKey publicKeys map[IssuerIdentifier]map[int]*gabi.PublicKey
reverseHashes map[string]CredentialTypeIdentifier reverseHashes map[string]CredentialTypeIdentifier
path string path string
initialized bool initialized bool
...@@ -32,19 +38,13 @@ func NewConfigurationStore(path string, assets string) (store *ConfigurationStor ...@@ -32,19 +38,13 @@ func NewConfigurationStore(path string, assets string) (store *ConfigurationStor
path: path, path: path,
} }
var exist bool if err = ensureDirectoryExists(store.path); err != nil {
if exist, err = PathExists(store.path); err != nil {
return nil, err return nil, err
} }
if !exist { if assets != "" {
if err = ensureDirectoryExists(store.path); err != nil { if err = store.Copy(assets, false); err != nil {
return nil, err return nil, err
} }
if assets != "" {
if err = store.Copy(assets, false); err != nil {
return nil, err
}
}
} }
return return
...@@ -57,7 +57,8 @@ func (store *ConfigurationStore) ParseFolder() error { ...@@ -57,7 +57,8 @@ func (store *ConfigurationStore) ParseFolder() error {
store.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager) store.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
store.Issuers = make(map[IssuerIdentifier]*Issuer) store.Issuers = make(map[IssuerIdentifier]*Issuer)
store.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType) store.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
store.publicKeys = make(map[IssuerIdentifier][]*gabi.PublicKey) store.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
store.reverseHashes = make(map[string]CredentialTypeIdentifier) store.reverseHashes = make(map[string]CredentialTypeIdentifier)
err := iterateSubfolders(store.path, func(dir string) error { err := iterateSubfolders(store.path, func(dir string) error {
...@@ -82,19 +83,12 @@ func (store *ConfigurationStore) ParseFolder() error { ...@@ -82,19 +83,12 @@ func (store *ConfigurationStore) ParseFolder() error {
// PublicKey returns the specified public key, or nil if not present in the ConfigurationStore. // PublicKey returns the specified public key, or nil if not present in the ConfigurationStore.
func (store *ConfigurationStore) PublicKey(id IssuerIdentifier, counter int) (*gabi.PublicKey, error) { func (store *ConfigurationStore) PublicKey(id IssuerIdentifier, counter int) (*gabi.PublicKey, error) {
if _, contains := store.publicKeys[id]; !contains { if _, contains := store.publicKeys[id]; !contains {
store.publicKeys[id] = []*gabi.PublicKey{} store.publicKeys[id] = map[int]*gabi.PublicKey{}
err := store.parseKeysFolder(id) if err := store.parseKeysFolder(id); err != nil {
if err != nil {
return nil, err return nil, err
} }
} }
return store.publicKeys[id][counter], nil
list := store.publicKeys[id]
if len(list) > counter {
return list[counter], nil
}
return nil, nil
} }
func (store *ConfigurationStore) addReverseHash(credid CredentialTypeIdentifier) { func (store *ConfigurationStore) addReverseHash(credid CredentialTypeIdentifier) {
...@@ -131,27 +125,33 @@ func (store *ConfigurationStore) parseIssuerFolders(path string) error { ...@@ -131,27 +125,33 @@ func (store *ConfigurationStore) parseIssuerFolders(path string) error {
}) })
} }
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
func (store *ConfigurationStore) parseKeysFolder(issuerid IssuerIdentifier) error { func (store *ConfigurationStore) parseKeysFolder(issuerid IssuerIdentifier) error {
issuer := store.Issuers[issuerid] path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", store.path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
path := store.path + "/" + files, err := filepath.Glob(path)
issuer.Identifier().SchemeManagerIdentifier().String() + if err != nil {
"/" + issuer.ID + "/PublicKeys/" return err
}
for i := 0; ; i++ {
file := path + strconv.Itoa(i) + ".xml" for _, file := range files {
if _, err := os.Stat(file); err != nil { filename := filepath.Base(file)
break count := filename[:len(filename)-4]
i, err := strconv.Atoi(count)
if err != nil {
continue
} }
pk, err := gabi.NewPublicKeyFromFile(file) pk, err := gabi.NewPublicKeyFromFile(file)
if err != nil { if err != nil {
return err return err
} }
pk.Issuer = issuer.Identifier().String() pk.Issuer = issuerid.String()
store.publicKeys[issuer.Identifier()] = append(store.publicKeys[issuer.Identifier()], pk) store.publicKeys[issuerid][i] = pk
} }
return nil return nil
} }
// parse $schememanager/$issuer/Issues/*/description.xml
func (store *ConfigurationStore) parseCredentialsFolder(path string) error { func (store *ConfigurationStore) parseCredentialsFolder(path string) error {
return iterateSubfolders(path, func(dir string) error { return iterateSubfolders(path, func(dir string) error {
cred := &CredentialType{} cred := &CredentialType{}
...@@ -266,3 +266,114 @@ func (store *ConfigurationStore) Copy(source string, parse bool) error { ...@@ -266,3 +266,114 @@ func (store *ConfigurationStore) Copy(source string, parse bool) error {
} }
return nil return nil
} }
func (store *ConfigurationStore) DownloadSchemeManager(url string) (*SchemeManager, error) {
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = "https://" + url
}
if url[len(url)-1] == '/' {
url = url[:len(url)-1]
}
if strings.HasSuffix(url, "/description.xml") {
url = url[:len(url)-len("/description.xml")]
}
b, err := NewHTTPTransport(url).GetBytes("/description.xml")
if err != nil {
return nil, err
}
manager := &SchemeManager{}
if err = xml.Unmarshal(b, manager); err != nil {
return nil, err
}
manager.URL = url // TODO?
return manager, nil
}
func (store *ConfigurationStore) RemoveSchemeManager(id SchemeManagerIdentifier) error {
// Remove everything falling under the manager's responsibility
for credid := range store.CredentialTypes {
if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
delete(store.CredentialTypes, credid)
}
}
for issid := range store.Issuers {
if issid.SchemeManagerIdentifier() == id {
delete(store.Issuers, issid)
}
}
for issid := range store.publicKeys {
if issid.SchemeManagerIdentifier() == id {
delete(store.publicKeys, issid)
}
}
// Remove from storage
return os.RemoveAll(fmt.Sprintf("%s/%s", store.path, id.String()))
// or, remove above iterations and call .ParseFolder()?
}
func (store *ConfigurationStore) AddSchemeManager(manager *SchemeManager) error {
name := manager.ID
if err := ensureDirectoryExists(fmt.Sprintf("%s/%s", store.path, name)); err != nil {
return err
}
b, err := xml.Marshal(manager)
if err != nil {
return err
}
if err := saveFile(fmt.Sprintf("%s/%s/description.xml", store.path, name), b); err != nil {
return err
}
store.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager
return nil
}
func (store *ConfigurationStore) Download(set *IrmaIdentifierSet) error {
var contains bool
for manid := range set.SchemeManagers {
if _, contains = store.SchemeManagers[manid]; !contains {
return errors.Errorf("Unknown scheme manager: %s", manid)
}
}
transport := NewHTTPTransport("")
for issid := range set.Issuers {
if _, contains = store.Issuers[issid]; !contains {
url := store.SchemeManagers[issid.SchemeManagerIdentifier()].URL + "/" + issid.Name()
path := fmt.Sprintf("%s/%s/%s", store.path, issid.SchemeManagerIdentifier().String(), issid.Name())
transport.GetFile(url+"/description.xml", path+"/description.xml")
transport.GetFile(url+"/logo.png", path+"/logo.png")
}
for issid, list := range set.PublicKeys {
for _, count := range list {
pk, err := store.PublicKey(issid, count)
if err != nil {
return err
}
if pk == nil {
manager := issid.SchemeManagerIdentifier()
suffix := fmt.Sprintf("/%s/PublicKeys/%d.xml", issid.Name(), count)
path := fmt.Sprintf("%s/%s/%s", store.path, manager.String(), suffix)
transport.GetFile(store.SchemeManagers[manager].URL+suffix, path)
}
}
}
}
for credid := range set.CredentialTypes {
if _, contains := store.CredentialTypes[credid]; !contains {
issuer := credid.IssuerIdentifier()
manager := issuer.SchemeManagerIdentifier()
local := fmt.Sprintf("%s/%s/%s/Issues", store.path, manager.Name(), issuer.Name())
if err := ensureDirectoryExists(local); err != nil {
return err
}
transport.GetFile(
fmt.Sprintf("%s/%s/Issues/%s/description.xml",
store.SchemeManagers[manager].URL, issuer.Name(), credid.Name()),
fmt.Sprintf("%s/%s/description.xml", local, credid.Name()),
)
}
}
return store.ParseFolder()
}
...@@ -6,11 +6,15 @@ import ( ...@@ -6,11 +6,15 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
) )
// This file contains data types for scheme managers, issuers, credential types
// matching the XML files in irma_configuration.
// SchemeManager describes a scheme manager. // SchemeManager describes a scheme manager.
type SchemeManager struct { type SchemeManager struct {
ID string `xml:"Id"` ID string `xml:"Id"`
Name TranslatedString `xml:"Name"` Name TranslatedString `xml:"Name"`
URL string `xml:"Contact"` URL string `xml:"Url"`
Contact string `xml:"contact"`
Description TranslatedString Description TranslatedString
KeyshareServer string KeyshareServer string
KeyshareWebsite string KeyshareWebsite string
...@@ -83,6 +87,26 @@ func (ct CredentialType) IndexOf(ai AttributeTypeIdentifier) (int, error) { ...@@ -83,6 +87,26 @@ func (ct CredentialType) IndexOf(ai AttributeTypeIdentifier) (int, error) {
// TranslatedString is a map of translated strings. // TranslatedString is a map of translated strings.
type TranslatedString map[string]string type TranslatedString map[string]string
type xmlTranslation struct {
XMLName xml.Name
Text string `xml:",chardata"`
}
type xmlTranslatedString struct {
Translations []xmlTranslation `xml:",any"`
}
// MarshalXML implements xml.Marshaler.
func (ts *TranslatedString) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
temp := &xmlTranslatedString{}
for lang, text := range *ts {
temp.Translations = append(temp.Translations,
xmlTranslation{XMLName: xml.Name{Local: lang}, Text: text},
)
}
return e.EncodeElement(temp, start)
}
// UnmarshalXML unmarshals an XML tag containing a string translated to multiple languages, // UnmarshalXML unmarshals an XML tag containing a string translated to multiple languages,
// for example: <Foo><en>Hello world</en><nl>Hallo wereld</nl></Foo> // for example: <Foo><en>Hello world</en><nl>Hallo wereld</nl></Foo>
// into a TranslatedString: { "en": "Hello world" , "nl": "Hallo wereld" } // into a TranslatedString: { "en": "Hello world" , "nl": "Hallo wereld" }
...@@ -90,12 +114,7 @@ func (ts *TranslatedString) UnmarshalXML(d *xml.Decoder, start xml.StartElement) ...@@ -90,12 +114,7 @@ func (ts *TranslatedString) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
if map[string]string(*ts) == nil { if map[string]string(*ts) == nil {
*ts = TranslatedString(make(map[string]string)) *ts = TranslatedString(make(map[string]string))
} }
temp := &struct { temp := &xmlTranslatedString{}
Translations []struct {
XMLName xml.Name
Text string `xml:",chardata"`
} `xml:",any"`
}{}
if err := d.DecodeElement(temp, &start); err != nil { if err := d.DecodeElement(temp, &start); err != nil {
return err return err
} }
......
...@@ -38,6 +38,15 @@ type AttributeIdentifier struct { ...@@ -38,6 +38,15 @@ type AttributeIdentifier struct {
Count int Count int
} }
// IrmaIdentifierSet contains a set (ensured by using map[...]struct{}) of all scheme managers,
// all issuers, all credential types and all public keys that are involved in an IRMA session.
type IrmaIdentifierSet struct {
SchemeManagers map[SchemeManagerIdentifier]struct{}
Issuers map[IssuerIdentifier]struct{}
CredentialTypes map[CredentialTypeIdentifier]struct{}
PublicKeys map[IssuerIdentifier][]int
}
// Parent returns the parent object of this identifier. // Parent returns the parent object of this identifier.
func (oi metaObjectIdentifier) Parent() string { func (oi metaObjectIdentifier) Parent() string {
str := string(oi) str := string(oi)
...@@ -145,3 +154,12 @@ func (id *AttributeTypeIdentifier) UnmarshalText(text []byte) error { ...@@ -145,3 +154,12 @@ func (id *AttributeTypeIdentifier) UnmarshalText(text []byte) error {
*id = NewAttributeTypeIdentifier(string(text)) *id = NewAttributeTypeIdentifier(string(text))
return nil return nil
} }
func (set *IrmaIdentifierSet) Distributed(store *ConfigurationStore) bool {
for id := range set.SchemeManagers {
if store.SchemeManagers[id].Distributed() {
return true
}
}
return false
}
...@@ -29,8 +29,10 @@ func TestMain(m *testing.M) { ...@@ -29,8 +29,10 @@ func TestMain(m *testing.M) {
type IgnoringKeyshareHandler struct{} type IgnoringKeyshareHandler struct{}
func (i *IgnoringKeyshareHandler) StartRegistration(m *SchemeManager, callback func(e, p string) error) { func (i *IgnoringKeyshareHandler) StartRegistration(m *SchemeManager, callback func(e, p string)) {
} }
func (i *IgnoringKeyshareHandler) RegistrationError(err error) {}
func (i *IgnoringKeyshareHandler) RegistrationSuccess() {}
func parseStorage(t *testing.T) *CredentialManager { func parseStorage(t *testing.T) *CredentialManager {
exists, err := PathExists("testdata/storage/test") exists, err := PathExists("testdata/storage/test")
...@@ -437,3 +439,19 @@ func TestCredentialRemoval(t *testing.T) { ...@@ -437,3 +439,19 @@ func TestCredentialRemoval(t *testing.T) {
teardown(t) teardown(t)
} }
func TestDownloadSchemeManager(t *testing.T) {
manager := parseStorage(t)
require.NoError(t, manager.ConfigurationStore.RemoveSchemeManager(NewSchemeManagerIdentifier("irma-demo")))
url := "https://raw.githubusercontent.com/credentials/irma_configuration/translate/irma-demo"
sm, err := manager.ConfigurationStore.DownloadSchemeManager(url)
require.NoError(t, err)
require.NotNil(t, sm)
require.NoError(t, manager.ConfigurationStore.AddSchemeManager(sm))
jwt := getIssuanceJwt("testip", NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID"))
sessionHelper(t, jwt, "issue", manager)
teardown(t)
}
...@@ -36,7 +36,6 @@ type keyshareSession struct { ...@@ -36,7 +36,6 @@ type keyshareSession struct {
keyshareServers map[SchemeManagerIdentifier]*keyshareServer keyshareServers map[SchemeManagerIdentifier]*keyshareServer
keyshareServer *keyshareServer // The one keyshare server in use in case of issuance keyshareServer *keyshareServer // The one keyshare server in use in case of issuance
transports map[SchemeManagerIdentifier]*HTTPTransport transports map[SchemeManagerIdentifier]*HTTPTransport
schemeManagers []SchemeManagerIdentifier
} }
type keyshareServer struct { type keyshareServer struct {
...@@ -99,7 +98,9 @@ type proofPCommitmentMap struct { ...@@ -99,7 +98,9 @@ type proofPCommitmentMap struct {
// KeyshareHandler is used for asking the user for his email address and PIN, // KeyshareHandler is used for asking the user for his email address and PIN,
// for registering at a keyshare server. // for registering at a keyshare server.
type KeyshareHandler interface { type KeyshareHandler interface {
StartRegistration(manager *SchemeManager, registrationCallback func(email, pin string) error) StartRegistration(manager *SchemeManager, registrationCallback func(email, pin string))
RegistrationError(err error)
RegistrationSuccess()
} }
const ( const (
...@@ -144,9 +145,8 @@ func startKeyshareSession( ...@@ -144,9 +145,8 @@ func startKeyshareSession(
store *ConfigurationStore, store *ConfigurationStore,
keyshareServers map[SchemeManagerIdentifier]*keyshareServer, keyshareServers map[SchemeManagerIdentifier]*keyshareServer,
) { ) {
schemeManagers := session.SchemeManagers()
ksscount := 0 ksscount := 0
for _, managerID := range schemeManagers { for managerID := range session.Identifiers().SchemeManagers {
if store.SchemeManagers[managerID].Distributed() { if store.SchemeManagers[managerID].Distributed() {
ksscount++ ksscount++
if _, registered := keyshareServers[managerID]; !registered { if _, registered := keyshareServers[managerID]; !registered {
...@@ -170,12 +170,11 @@ func startKeyshareSession( ...@@ -170,12 +170,11 @@ func startKeyshareSession(
pinRequestor: pin, pinRequestor: pin,
store: store, store: store,
keyshareServers: keyshareServers, keyshareServers: keyshareServers,
schemeManagers: schemeManagers,
} }
requestPin := false requestPin := false
for _, managerID := range schemeManagers { for managerID := range session.Identifiers().SchemeManagers {
if !ks.store.SchemeManagers[managerID].Distributed() { if !ks.store.SchemeManagers[managerID].Distributed() {
continue continue
} }
...@@ -243,7 +242,7 @@ func (ks *keyshareSession) VerifyPin(attempts int) { ...@@ -243,7 +242,7 @@ func (ks *keyshareSession) VerifyPin(attempts int) {
// - If this or anything else (specified in err) goes wrong, success will be false. // - If this or anything else (specified in err) goes wrong, success will be false.
// If all is ok, success will be true. // If all is ok, success will be true.
func (ks *keyshareSession) verifyPinAttempt(pin string) (success bool, tries int, blocked int, err error) { func (ks *keyshareSession) verifyPinAttempt(pin string) (success bool, tries int, blocked int, err error) {
for _, managerID := range ks.schemeManagers { for managerID := range ks.session.Identifiers().SchemeManagers {
if !ks.store.SchemeManagers[managerID].Distributed() { if !ks.store.SchemeManagers[managerID].Distributed() {
continue continue
} }
...@@ -306,7 +305,7 @@ func (ks *keyshareSession) GetCommitments() { ...@@ -306,7 +305,7 @@ func (ks *keyshareSession) GetCommitments() {
// Now inform each keyshare server of with respect to which public keys // Now inform each keyshare server of with respect to which public keys
// we want them to send us commitments // we want them to send us commitments
for _, managerID := range ks.schemeManagers { for managerID := range ks.session.Identifiers().SchemeManagers {
if !ks.store.SchemeManagers[managerID].Distributed() { if !ks.store.SchemeManagers[managerID].Distributed() {
continue continue
} }
...@@ -357,7 +356,7 @@ func (ks *keyshareSession) GetProofPs() { ...@@ -357,7 +356,7 @@ func (ks *keyshareSession) GetProofPs() {
// Post the challenge, obtaining JWT's containing the ProofP's // Post the challenge, obtaining JWT's containing the ProofP's
responses := map[SchemeManagerIdentifier]string{} responses := map[SchemeManagerIdentifier]string{}
for _, managerID := range ks.schemeManagers { for managerID := range ks.session.Identifiers().SchemeManagers {
transport, distributed := ks.transports[managerID] transport, distributed := ks.transports[managerID]
if !distributed { if !distributed {
continue continue
......
...@@ -49,25 +49,13 @@ type CredentialManager struct { ...@@ -49,25 +49,13 @@ type CredentialManager struct {
ConfigurationStore *ConfigurationStore ConfigurationStore *ConfigurationStore
irmaConfigurationPath string irmaConfigurationPath string
androidStoragePath string androidStoragePath string
keyshareHandler KeyshareHandler
} }
type secretKey struct { type secretKey struct {
Key *big.Int Key *big.Int
} }
// IrmaSession is an IRMA session.
type IrmaSession interface {
GetNonce() *big.Int
SetNonce(*big.Int)
GetContext() *big.Int
SetContext(*big.Int)
ToDisclose() AttributeDisjunctionList
DisclosureChoice() *DisclosureChoice
SetDisclosureChoice(choice *DisclosureChoice)
Distributed(store *ConfigurationStore) bool
SchemeManagers() []SchemeManagerIdentifier
}
// NewCredentialManager creates a new CredentialManager that uses the directory // NewCredentialManager creates a new CredentialManager that uses the directory
// specified by storagePath for (de)serializing itself. irmaConfigurationPath // specified by storagePath for (de)serializing itself. irmaConfigurationPath
// is the path to a (possibly readonly) folder containing irma_configuration; // is the path to a (possibly readonly) folder containing irma_configuration;
...@@ -94,25 +82,25 @@ func NewCredentialManager( ...@@ -94,25 +82,25 @@ func NewCredentialManager(
return nil, err return nil, err
} }
var store *ConfigurationStore
if store, err = NewConfigurationStore(storagePath+"/irma_configuration", irmaConfigurationPath); err != nil {
return nil, err
}
if err = store.ParseFolder(); err != nil {
return nil, err
}
cm := &CredentialManager{ cm := &CredentialManager{
credentials: make(map[CredentialTypeIdentifier]map[int]*credential), credentials: make(map[CredentialTypeIdentifier]map[int]*credential),
keyshareServers: make(map[SchemeManagerIdentifier]*keyshareServer), keyshareServers: make(map[SchemeManagerIdentifier]*keyshareServer),
attributes: make(map[CredentialTypeIdentifier][]*AttributeList), attributes: make(map[CredentialTypeIdentifier][]*AttributeList),
irmaConfigurationPath: irmaConfigurationPath, irmaConfigurationPath: irmaConfigurationPath,
androidStoragePath: androidStoragePath, androidStoragePath: androidStoragePath,
ConfigurationStore: store, keyshareHandler: keyshareHandler,
storage: storage{storagePath: storagePath, ConfigurationStore: store}, }
cm.ConfigurationStore, err = NewConfigurationStore(storagePath+"/irma_configuration", irmaConfigurationPath)
if err != nil {
return nil, err
}
if err = cm.ConfigurationStore.ParseFolder(); err != nil {
return nil, err
} }
// Ensure storage path exists, and populate it with necessary files // Ensure storage path exists, and populate it with necessary files
cm.storage = storage{storagePath: storagePath, ConfigurationStore: cm.ConfigurationStore}
if err = cm.storage.EnsureStorageExists(); err != nil { if err = cm.storage.EnsureStorageExists(); err != nil {
return nil, err return nil, err
}