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 (
"crypto/sha256"
"fmt"
"strings"
"github.com/go-errors/errors"
"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 {
SchemeManagers map[SchemeManagerIdentifier]*SchemeManager
Issuers map[IssuerIdentifier]*Issuer
CredentialTypes map[CredentialTypeIdentifier]*CredentialType
publicKeys map[IssuerIdentifier][]*gabi.PublicKey
publicKeys map[IssuerIdentifier]map[int]*gabi.PublicKey
reverseHashes map[string]CredentialTypeIdentifier
path string
initialized bool
......@@ -32,19 +38,13 @@ func NewConfigurationStore(path string, assets string) (store *ConfigurationStor
path: path,
}
var exist bool
if exist, err = PathExists(store.path); err != nil {
if err = ensureDirectoryExists(store.path); err != nil {
return nil, err
}
if !exist {
if err = ensureDirectoryExists(store.path); err != nil {
if assets != "" {
if err = store.Copy(assets, false); err != nil {
return nil, err
}
if assets != "" {
if err = store.Copy(assets, false); err != nil {
return nil, err
}
}
}
return
......@@ -57,7 +57,8 @@ func (store *ConfigurationStore) ParseFolder() error {
store.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
store.Issuers = make(map[IssuerIdentifier]*Issuer)
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)
err := iterateSubfolders(store.path, func(dir string) error {
......@@ -82,19 +83,12 @@ func (store *ConfigurationStore) ParseFolder() error {
// 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) {
if _, contains := store.publicKeys[id]; !contains {
store.publicKeys[id] = []*gabi.PublicKey{}
err := store.parseKeysFolder(id)
if err != nil {
store.publicKeys[id] = map[int]*gabi.PublicKey{}
if err := store.parseKeysFolder(id); err != nil {
return nil, err
}
}
list := store.publicKeys[id]
if len(list) > counter {
return list[counter], nil
}
return nil, nil
return store.publicKeys[id][counter], nil
}
func (store *ConfigurationStore) addReverseHash(credid CredentialTypeIdentifier) {
......@@ -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 {
issuer := store.Issuers[issuerid]
path := store.path + "/" +
issuer.Identifier().SchemeManagerIdentifier().String() +
"/" + issuer.ID + "/PublicKeys/"
for i := 0; ; i++ {
file := path + strconv.Itoa(i) + ".xml"
if _, err := os.Stat(file); err != nil {
break
path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", store.path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
files, err := filepath.Glob(path)
if err != nil {
return err
}
for _, file := range files {
filename := filepath.Base(file)
count := filename[:len(filename)-4]
i, err := strconv.Atoi(count)
if err != nil {
continue
}
pk, err := gabi.NewPublicKeyFromFile(file)
if err != nil {
return err
}
pk.Issuer = issuer.Identifier().String()
store.publicKeys[issuer.Identifier()] = append(store.publicKeys[issuer.Identifier()], pk)
pk.Issuer = issuerid.String()
store.publicKeys[issuerid][i] = pk
}
return nil
}
// parse $schememanager/$issuer/Issues/*/description.xml
func (store *ConfigurationStore) parseCredentialsFolder(path string) error {
return iterateSubfolders(path, func(dir string) error {
cred := &CredentialType{}
......@@ -266,3 +266,114 @@ func (store *ConfigurationStore) Copy(source string, parse bool) error {
}
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 (
"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.
type SchemeManager struct {
ID string `xml:"Id"`
Name TranslatedString `xml:"Name"`
URL string `xml:"Contact"`
URL string `xml:"Url"`
Contact string `xml:"contact"`
Description TranslatedString
KeyshareServer string
KeyshareWebsite string
......@@ -83,6 +87,26 @@ func (ct CredentialType) IndexOf(ai AttributeTypeIdentifier) (int, error) {
// TranslatedString is a map of translated strings.
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,
// for example: <Foo><en>Hello world</en><nl>Hallo wereld</nl></Foo>
// into a TranslatedString: { "en": "Hello world" , "nl": "Hallo wereld" }
......@@ -90,12 +114,7 @@ func (ts *TranslatedString) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
if map[string]string(*ts) == nil {
*ts = TranslatedString(make(map[string]string))
}
temp := &struct {
Translations []struct {
XMLName xml.Name
Text string `xml:",chardata"`
} `xml:",any"`
}{}
temp := &xmlTranslatedString{}
if err := d.DecodeElement(temp, &start); err != nil {
return err
}
......
......@@ -38,6 +38,15 @@ type AttributeIdentifier struct {
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.
func (oi metaObjectIdentifier) Parent() string {
str := string(oi)
......@@ -145,3 +154,12 @@ func (id *AttributeTypeIdentifier) UnmarshalText(text []byte) error {
*id = NewAttributeTypeIdentifier(string(text))
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) {
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 {
exists, err := PathExists("testdata/storage/test")
......@@ -437,3 +439,19 @@ func TestCredentialRemoval(t *testing.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 {
keyshareServers map[SchemeManagerIdentifier]*keyshareServer
keyshareServer *keyshareServer // The one keyshare server in use in case of issuance
transports map[SchemeManagerIdentifier]*HTTPTransport
schemeManagers []SchemeManagerIdentifier
}
type keyshareServer struct {
......@@ -99,7 +98,9 @@ type proofPCommitmentMap struct {
// KeyshareHandler is used for asking the user for his email address and PIN,
// for registering at a keyshare server.
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 (
......@@ -144,9 +145,8 @@ func startKeyshareSession(
store *ConfigurationStore,
keyshareServers map[SchemeManagerIdentifier]*keyshareServer,
) {
schemeManagers := session.SchemeManagers()
ksscount := 0
for _, managerID := range schemeManagers {
for managerID := range session.Identifiers().SchemeManagers {
if store.SchemeManagers[managerID].Distributed() {
ksscount++
if _, registered := keyshareServers[managerID]; !registered {
......@@ -170,12 +170,11 @@ func startKeyshareSession(
pinRequestor: pin,
store: store,
keyshareServers: keyshareServers,
schemeManagers: schemeManagers,
}
requestPin := false
for _, managerID := range schemeManagers {
for managerID := range session.Identifiers().SchemeManagers {
if !ks.store.SchemeManagers[managerID].Distributed() {
continue
}
......@@ -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 all is ok, success will be true.
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() {
continue
}
......@@ -306,7 +305,7 @@ func (ks *keyshareSession) GetCommitments() {
// Now inform each keyshare server of with respect to which public keys
// 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() {
continue
}
......@@ -357,7 +356,7 @@ func (ks *keyshareSession) GetProofPs() {
// Post the challenge, obtaining JWT's containing the ProofP's
responses := map[SchemeManagerIdentifier]string{}
for _, managerID := range ks.schemeManagers {
for managerID := range ks.session.Identifiers().SchemeManagers {
transport, distributed := ks.transports[managerID]
if !distributed {
continue
......
......@@ -49,25 +49,13 @@ type CredentialManager struct {
ConfigurationStore *ConfigurationStore
irmaConfigurationPath string
androidStoragePath string
keyshareHandler KeyshareHandler
}
type secretKey struct {
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
// specified by storagePath for (de)serializing itself. irmaConfigurationPath
// is the path to a (possibly readonly) folder containing irma_configuration;
......@@ -94,25 +82,25 @@ func NewCredentialManager(
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{
credentials: make(map[CredentialTypeIdentifier]map[int]*credential),
keyshareServers: make(map[SchemeManagerIdentifier]*keyshareServer),
attributes: make(map[CredentialTypeIdentifier][]*AttributeList),
irmaConfigurationPath: irmaConfigurationPath,
androidStoragePath: androidStoragePath,
ConfigurationStore: store,
storage: storage{storagePath: storagePath, ConfigurationStore: store},
keyshareHandler: keyshareHandler,
}
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
cm.storage = storage{storagePath: storagePath, ConfigurationStore: cm.ConfigurationStore}
if err = cm.storage.EnsureStorageExists(); err != nil {
return nil, err
}
......@@ -143,9 +131,7 @@ func NewCredentialManager(
if keyshareHandler == nil {
return nil, errors.New("Keyshare server found but no KeyshareHandler was given")
}
keyshareHandler.StartRegistration(unenrolled[0], func(email, pin string) error {
return cm.KeyshareEnroll(unenrolled[0].Identifier(), email, pin)
})
cm.KeyshareEnroll(unenrolled[0], keyshareHandler)
default:
return nil, errors.New("Too many keyshare servers")
}
......@@ -582,7 +568,20 @@ func (cm *CredentialManager) unenrolledKeyshareServers() []*SchemeManager {
}
// KeyshareEnroll attempts to register at the keyshare server of the specified scheme manager.
func (cm *CredentialManager) KeyshareEnroll(managerID SchemeManagerIdentifier, email, pin string) error {
func (cm *CredentialManager) KeyshareEnroll(manager *SchemeManager, handler KeyshareHandler) {
handler.StartRegistration(manager, func(email, pin string) {
go func() {
err := cm.keyshareEnrollWorker(manager.Identifier(), email, pin)
if err != nil {
handler.RegistrationError(err)
} else {
handler.RegistrationSuccess()
}
}()
})
}
func (cm *CredentialManager) keyshareEnrollWorker(managerID SchemeManagerIdentifier, email, pin string) error {
manager, ok := cm.ConfigurationStore.SchemeManagers[managerID]
if !ok {
return errors.New("Unknown scheme manager")
......
......@@ -68,10 +68,11 @@ const (
// Actions
const (
ActionDisclosing = Action("disclosing")
ActionSigning = Action("signing")
ActionIssuing = Action("issuing")
ActionUnknown = Action("unknown")
ActionSchemeManager = Action("schememanager")
ActionDisclosing = Action("disclosing")
ActionSigning = Action("signing")
ActionIssuing = Action("issuing")
ActionUnknown = Action("unknown")
)
// Protocol errors
......@@ -98,6 +99,8 @@ const (
ErrorApi = ErrorType("api")
// Server returned unexpected or malformed response
ErrorServerResponse = ErrorType("serverResponse")
// Error during downloading of credential type, issuer, or public keys
ErrorConfigurationStoreDownload = ErrorType("configurationStoreDownload")
)
func (e *SessionError) Error() string {
......
......@@ -15,9 +15,10 @@ import (
// SessionRequest contains the context and nonce for an IRMA session.
type SessionRequest struct {
Context *big.Int `json:"context"`
Nonce *big.Int `json:"nonce"`
choice *DisclosureChoice
Context *big.Int `json:"context"`
Nonce *big.Int `json:"nonce"`
choice *DisclosureChoice
identifiers *IrmaIdentifierSet
}
// DisclosureChoice returns the attributes to be disclosed in this session.
......@@ -102,6 +103,18 @@ type IdentityProviderJwt struct {
Request IdentityProviderRequest `json:"iprequest"`
}
// 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)
Identifiers() *IrmaIdentifierSet
}
// Timestamp is a time.Time that marshals to Unix timestamps.
type Timestamp time.Time
......@@ -153,28 +166,35 @@ func newIssuanceState() (*issuanceState, error) {
}, nil
}
// Distributed indicates if a keyshare is involved in this session.
func (ir *IssuanceRequest) Distributed(store *ConfigurationStore) bool {
for _, manager := range ir.SchemeManagers() {
if store.SchemeManagers[manager].Distributed() {
return true
func (dr *IssuanceRequest) Identifiers() *IrmaIdentifierSet {
if dr.identifiers == nil {
dr.identifiers = &IrmaIdentifierSet{
SchemeManagers: map[SchemeManagerIdentifier]struct{}{},
Issuers: map[IssuerIdentifier]struct{}{},
CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
PublicKeys: map[IssuerIdentifier][]int{},
}
}
return false
}
// SchemeManagers returns a list of all scheme managers involved in this session.
func (ir *IssuanceRequest) SchemeManagers() []SchemeManagerIdentifier {
list := []SchemeManagerIdentifier{}
for _, cred := range ir.Credentials {
list = append(list, cred.Credential.IssuerIdentifier().SchemeManagerIdentifier())
}
for _, disjunctions := range ir.Disclose {
for _, attr := range disjunctions.Attributes {
list = append(list, attr.CredentialTypeIdentifier().IssuerIdentifier().SchemeManagerIdentifier())
for _, credreq := range dr.Credentials {
issuer := credreq.Credential.IssuerIdentifier()
dr.identifiers.SchemeManagers[issuer.SchemeManagerIdentifier()] = struct{}{}
dr.identifiers.Issuers[issuer] = struct{}{}
dr.identifiers.CredentialTypes[*credreq.Credential] = struct{}{}
if dr.identifiers.PublicKeys[issuer] == nil {
dr.identifiers.PublicKeys[issuer] = []int{}
}
dr.identifiers.PublicKeys[issuer] = append(dr.identifiers.PublicKeys[issuer], credreq.KeyCounter)
}