...
 
Commits (31)
......@@ -40,12 +40,13 @@ type AttributeIdentifier struct {
}
// 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.
// all issuers, all credential types, all public keys and all attribute types 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
AttributeTypes map[AttributeTypeIdentifier]struct{}
}
func newIrmaIdentifierSet() *IrmaIdentifierSet {
......@@ -54,6 +55,7 @@ func newIrmaIdentifierSet() *IrmaIdentifierSet {
Issuers: map[IssuerIdentifier]struct{}{},
CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
PublicKeys: map[IssuerIdentifier][]int{},
AttributeTypes: map[AttributeTypeIdentifier]struct{}{},
}
}
......@@ -194,6 +196,9 @@ func (set *IrmaIdentifierSet) join(other *IrmaIdentifierSet) {
for ct := range other.CredentialTypes {
set.CredentialTypes[ct] = struct{}{}
}
for at := range other.AttributeTypes {
set.AttributeTypes[at] = struct{}{}
}
for issuer := range other.PublicKeys {
if len(set.PublicKeys[issuer]) == 0 {
set.PublicKeys[issuer] = make([]int, 0, len(other.PublicKeys[issuer]))
......@@ -227,6 +232,9 @@ func (set *IrmaIdentifierSet) allSchemes() map[SchemeManagerIdentifier]struct{}
for c := range set.CredentialTypes {
schemes[c.IssuerIdentifier().SchemeManagerIdentifier()] = struct{}{}
}
for a := range set.AttributeTypes {
schemes[a.CredentialTypeIdentifier().IssuerIdentifier().SchemeManagerIdentifier()] = struct{}{}
}
return schemes
}
......@@ -246,6 +254,9 @@ func (set *IrmaIdentifierSet) String() string {
for c := range set.CredentialTypes {
builder.WriteString(c.String() + ", ")
}
for a := range set.AttributeTypes {
builder.WriteString(a.String() + ", ")
}
s := builder.String()
if len(s) > 0 { // strip trailing comma
s = s[:len(s)-2]
......@@ -254,5 +265,5 @@ func (set *IrmaIdentifierSet) String() string {
}
func (set *IrmaIdentifierSet) Empty() bool {
return len(set.SchemeManagers) == 0 && len(set.Issuers) == 0 && len(set.CredentialTypes) == 0 && len(set.PublicKeys) == 0
return len(set.SchemeManagers) == 0 && len(set.Issuers) == 0 && len(set.CredentialTypes) == 0 && len(set.PublicKeys) == 0 && len(set.AttributeTypes) == 0
}
......@@ -185,3 +185,50 @@ func Base64Decode(b []byte) ([]byte, error) {
}
return bts, err
}
// iterateSubfolders iterates over the subfolders of the specified path,
// calling the specified handler each time. If anything goes wrong, or
// if the caller returns a non-nil error, an error is immediately returned.
func IterateSubfolders(path string, handler func(string, os.FileInfo) error) error {
return iterateFiles(path, true, handler)
}
func iterateFiles(path string, onlyDirs bool, handler func(string, os.FileInfo) error) error {
files, err := filepath.Glob(filepath.Join(path, "*"))
if err != nil {
return err
}
for _, file := range files {
stat, err := os.Stat(file)
if err != nil {
return err
}
if onlyDirs && !stat.IsDir() {
continue
}
if filepath.Base(file) == ".git" {
continue
}
err = handler(file, stat)
if err != nil {
return err
}
}
return nil
}
// walkDir recursively walks the file tree rooted at path, following symlinks (unlike filepath.Walk).
// Avoiding loops is the responsibility of the caller.
func WalkDir(path string, handler func(string, os.FileInfo) error) error {
return iterateFiles(path, false, func(p string, info os.FileInfo) error {
if info.IsDir() {
if err := handler(p, info); err != nil {
return err
}
return WalkDir(p, handler)
}
return handler(p, info)
})
}
......@@ -94,11 +94,6 @@ func (s *Server) validateIssuanceRequest(request *irma.IssuanceRequest) error {
}
cred.KeyCounter = int(privatekey.Counter)
// Check that the credential is consistent with irma_configuration
if err := cred.Validate(s.conf.IrmaConfiguration); err != nil {
return err
}
// Ensure the credential has an expiry date
defaultValidity := irma.Timestamp(time.Now().AddDate(0, 6, 0))
if cred.Validity == nil {
......
package sessiontest
import (
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test"
"github.com/stretchr/testify/require"
"testing"
)
func TestSessionUsingLegacyStorage(t *testing.T) {
test.SetTestStorageDir("legacy_teststorage")
defer test.SetTestStorageDir("teststorage")
client, _ := parseStorage(t)
defer test.ClearTestStorage(t)
// Test whether credential from legacy storage is still usable
idStudentCard := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
request := getDisclosureRequest(idStudentCard)
sessionHelper(t, request, "verification", client)
// Issue new credential
sessionHelper(t, getMultipleIssuanceRequest(), "issue", client)
// Test whether credential is still there
idRoot := irma.NewAttributeTypeIdentifier("irma-demo.MijnOverheid.root.BSN")
sessionHelper(t, getDisclosureRequest(idRoot), "verification", client)
// Re-open client
require.NoError(t, client.Close())
client, _ = parseExistingStorage(t)
// Test whether credential is still there after the storage has been reloaded
sessionHelper(t, getDisclosureRequest(idRoot), "verification", client)
}
......@@ -25,6 +25,13 @@ func TestLogging(t *testing.T) {
require.NoError(t, err)
require.True(t, len(logs) == oldLogLength+1)
// Check whether newly issued credential is actually stored
require.NoError(t, client.Close())
client, _ = parseExistingStorage(t)
logs, err = client.LoadNewestLogs(100)
require.NoError(t, err)
require.True(t, len(logs) == oldLogLength+1)
entry := logs[0]
require.NotNil(t, entry)
require.NoError(t, err)
......@@ -42,6 +49,13 @@ func TestLogging(t *testing.T) {
require.NoError(t, err)
require.True(t, len(logs) == oldLogLength+2)
// Check whether log entry for disclosing session is actually stored
require.NoError(t, client.Close())
client, _ = parseExistingStorage(t)
logs, err = client.LoadNewestLogs(100)
require.NoError(t, err)
require.True(t, len(logs) == oldLogLength+2)
entry = logs[0]
require.NotNil(t, entry)
require.NoError(t, err)
......@@ -66,6 +80,14 @@ func TestLogging(t *testing.T) {
logs, err = client.LoadNewestLogs(100)
require.NoError(t, err)
require.True(t, len(logs) == oldLogLength+3)
// Check whether log entry for signature session is actually stored
require.NoError(t, client.Close())
client, _ = parseExistingStorage(t)
logs, err = client.LoadNewestLogs(100)
require.NoError(t, err)
require.True(t, len(logs) == oldLogLength+3)
entry = logs[0]
require.NotNil(t, entry)
require.NoError(t, err)
......
......@@ -37,6 +37,10 @@ func TestMain(m *testing.M) {
func parseStorage(t *testing.T) (*irmaclient.Client, *TestClientHandler) {
test.SetupTestStorage(t)
return parseExistingStorage(t)
}
func parseExistingStorage(t *testing.T) (*irmaclient.Client, *TestClientHandler) {
handler := &TestClientHandler{t: t, c: make(chan error)}
path := test.FindTestdataFolder(t)
client, err := irmaclient.New(
......
......@@ -13,6 +13,7 @@ import (
"github.com/privacybydesign/irmago/internal/fs"
"github.com/privacybydesign/irmago/internal/test"
"github.com/privacybydesign/irmago/irmaclient"
"github.com/privacybydesign/irmago/server"
"github.com/stretchr/testify/require"
)
......@@ -108,6 +109,11 @@ func TestIssuanceSameAttributesNotSingleton(t *testing.T) {
req = getIssuanceRequest(false)
sessionHelper(t, req, "issue", client)
require.Equal(t, prevLen+1, len(client.CredentialInfoList()))
// Also check whether this is actually stored
require.NoError(t, client.Close())
client, _ = parseExistingStorage(t)
require.Equal(t, prevLen+1, len(client.CredentialInfoList()))
}
func TestLargeAttribute(t *testing.T) {
......@@ -139,6 +145,12 @@ func TestIssuanceSingletonCredential(t *testing.T) {
sessionHelper(t, request, "issue", client)
require.NotNil(t, client.Attributes(credid, 0))
require.Nil(t, client.Attributes(credid, 1))
// Also check whether this is actually stored
require.NoError(t, client.Close())
client, _ = parseExistingStorage(t)
require.NotNil(t, client.Attributes(credid, 0))
require.Nil(t, client.Attributes(credid, 1))
}
func TestUnsatisfiableDisclosureSession(t *testing.T) {
......@@ -289,13 +301,40 @@ func TestIssueOptionalAttributeUpdateSchemeManager(t *testing.T) {
credid := irma.NewCredentialTypeIdentifier("irma-demo.RU.studentCard")
attrid := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.level")
require.False(t, client.Configuration.CredentialTypes[credid].AttributeType(attrid).IsOptional())
client.Configuration.SchemeManagers[schemeid].URL = "http://localhost:48681/irma_configuration_updated/irma-demo"
issuanceRequest := getIssuanceRequest(true)
delete(issuanceRequest.Credentials[0].Attributes, "level")
_, err := client.Configuration.Download(issuanceRequest)
serverChan := make(chan *server.SessionResult)
StartIrmaServer(t, false) // Run a server with old configuration (level is non-optional)
_, _, err := irmaServer.StartSession(issuanceRequest, func(result *server.SessionResult) {
serverChan <- result
})
expectedError := &irma.RequiredAttributeMissingError{
ErrorType: irma.ErrorRequiredAttributeMissing,
Missing: &irma.IrmaIdentifierSet{
SchemeManagers: map[irma.SchemeManagerIdentifier]struct{}{},
Issuers: map[irma.IssuerIdentifier]struct{}{},
CredentialTypes: map[irma.CredentialTypeIdentifier]struct{}{},
PublicKeys: map[irma.IssuerIdentifier][]int{},
AttributeTypes: map[irma.AttributeTypeIdentifier]struct{}{
irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.level"): struct{}{},
},
},
}
require.True(t, reflect.DeepEqual(err, expectedError), "Incorrect missing identifierset")
StopIrmaServer()
StartIrmaServer(t, true) // Run a server with updated configuration (level is optional)
_, err = client.Configuration.Download(issuanceRequest)
require.NoError(t, err)
require.True(t, client.Configuration.CredentialTypes[credid].AttributeType(attrid).IsOptional())
_, _, err = irmaServer.StartSession(issuanceRequest, func(result *server.SessionResult) {
serverChan <- result
})
require.NoError(t, err)
StopIrmaServer()
}
func TestIssueNewCredTypeUpdateSchemeManager(t *testing.T) {
......@@ -337,8 +376,9 @@ func TestDisclosureNewCredTypeUpdateSchemeManager(t *testing.T) {
func TestDisclosureNonexistingCredTypeUpdateSchemeManager(t *testing.T) {
client, _ := parseStorage(t)
request := irma.NewDisclosureRequest(
irma.NewAttributeTypeIdentifier("irma-demo.RU.foo.bar"),
irma.NewAttributeTypeIdentifier("irma-demo.baz.qux.abc"),
irma.NewAttributeTypeIdentifier("irma-demo.baz.qux.abc"), // non-existing issuer
irma.NewAttributeTypeIdentifier("irma-demo.RU.foo.bar"), // non-existing credential
irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.xyz"), // non-existing attribute
)
_, err := client.Configuration.Download(request)
require.Error(t, err)
......@@ -355,6 +395,9 @@ func TestDisclosureNonexistingCredTypeUpdateSchemeManager(t *testing.T) {
irma.NewCredentialTypeIdentifier("irma-demo.RU.foo"): struct{}{},
irma.NewCredentialTypeIdentifier("irma-demo.baz.qux"): struct{}{},
},
AttributeTypes: map[irma.AttributeTypeIdentifier]struct{}{
irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.xyz"): struct{}{},
},
},
}
require.True(t, reflect.DeepEqual(expectedErr, err), "Download() returned incorrect missing identifier set")
......@@ -443,3 +486,16 @@ func TestStaticQRSession(t *testing.T) {
require.NoError(t, s.Shutdown(context.Background()))
require.True(t, received)
}
func TestIssuedCredentialIsStored(t *testing.T) {
client, _ := parseStorage(t)
defer test.ClearTestStorage(t)
issuanceRequest := getNameIssuanceRequest()
sessionHelper(t, issuanceRequest, "issue", client)
require.NoError(t, client.Close())
client, _ = parseExistingStorage(t)
id := irma.NewAttributeTypeIdentifier("irma-demo.MijnOverheid.fullName.familyname")
sessionHelper(t, getDisclosureRequest(id), "verification", client)
}
......@@ -30,6 +30,7 @@ func checkError(t *testing.T, err error) {
var schemeServer *http.Server
var badServer *http.Server
var badServerCount int
var testStorageDir = "teststorage"
func StartSchemeManagerHttpServer() {
path := FindTestdataFolder(nil)
......@@ -108,7 +109,7 @@ func CreateTestStorage(t *testing.T) {
func SetupTestStorage(t *testing.T) {
CreateTestStorage(t)
path := FindTestdataFolder(t)
err := fs.CopyDirectory(filepath.Join(path, "teststorage"), filepath.Join(path, "storage", "test"))
err := fs.CopyDirectory(filepath.Join(path, testStorageDir), filepath.Join(path, "storage", "test"))
checkError(t, err)
}
......@@ -117,3 +118,7 @@ func PrettyPrint(t *testing.T, ob interface{}) string {
require.NoError(t, err)
return string(b)
}
func SetTestStorageDir(dir string) {
testStorageDir = dir
}
......@@ -84,8 +84,8 @@ func signManager(privatekey *ecdsa.PrivateKey, confpath string, skipverification
// Traverse dir and add file hashes to index
var index irma.SchemeManagerIndex = make(map[string]irma.ConfigurationFileHash)
err := filepath.Walk(confpath, func(path string, info os.FileInfo, err error) error {
return calculateFileHash(path, info, err, confpath, index)
err := fs.WalkDir(confpath, func(path string, info os.FileInfo) error {
return calculateFileHash(path, info, confpath, index)
})
if err != nil {
return errors.WrapPrefix(err, "Failed to calculate file index:", 0)
......@@ -141,10 +141,7 @@ func readPrivateKey(path string) (*ecdsa.PrivateKey, error) {
return x509.ParseECPrivateKey(block.Bytes)
}
func calculateFileHash(path string, info os.FileInfo, err error, confpath string, index irma.SchemeManagerIndex) error {
if err != nil {
return err
}
func calculateFileHash(path string, info os.FileInfo, confpath string, index irma.SchemeManagerIndex) error {
// Skip stuff we don't want
if info.IsDir() || // Can only sign files
strings.HasSuffix(path, "index") || // Skip the index file itself
......
......@@ -48,6 +48,8 @@ type Client struct {
// Where we store/load it to/from
storage storage
// Legacy storage needed when client has not updated to the new storage yet
fileStorage fileStorage
// Other state
Preferences Preferences
......@@ -153,6 +155,8 @@ func New(
if err = cm.storage.EnsureStorageExists(); err != nil {
return nil, err
}
// Legacy storage does not need ensuring existence
cm.fileStorage = fileStorage{storagePath: storagePath, Configuration: cm.Configuration}
if cm.Preferences, err = cm.storage.LoadPreferences(); err != nil {
return nil, err
......@@ -182,6 +186,10 @@ func New(
return cm, schemeMgrErr
}
func (client *Client) Close() error {
return client.storage.Close()
}
// CredentialInfoList returns a list of information of all contained credentials.
func (client *Client) CredentialInfoList() irma.CredentialInfoList {
list := irma.CredentialInfoList([]*irma.CredentialInfo{})
......@@ -201,7 +209,7 @@ func (client *Client) CredentialInfoList() irma.CredentialInfoList {
// addCredential adds the specified credential to the Client, saving its signature
// imediately, and optionally cm.attributes as well.
func (client *Client) addCredential(cred *credential, storeAttributes bool) (err error) {
func (client *Client) addCredential(cred *credential) (err error) {
id := irma.NewCredentialTypeIdentifier("")
if cred.CredentialType() != nil {
id = cred.CredentialType().Identifier()
......@@ -221,13 +229,17 @@ func (client *Client) addCredential(cred *credential, storeAttributes bool) (err
if !id.Empty() {
if cred.CredentialType().IsSingleton {
for len(client.attrs(id)) != 0 {
_ = client.remove(id, 0, false)
if err = client.remove(id, 0, false); err != nil {
return
}
}
}
for i := len(client.attrs(id)) - 1; i >= 0; i-- { // Go backwards through array because remove manipulates it
if client.attrs(id)[i].EqualsExceptMetadata(cred.AttributeList()) {
_ = client.remove(id, i, false)
if err = client.remove(id, i, false); err != nil {
return
}
}
}
}
......@@ -242,13 +254,12 @@ func (client *Client) addCredential(cred *credential, storeAttributes bool) (err
client.credentialsCache[id][counter] = cred
}
if err = client.storage.StoreSignature(cred); err != nil {
return
}
if storeAttributes {
err = client.storage.StoreAttributes(client.attributes)
}
return
return client.storage.Transaction(func(tx *transaction) error {
if err = client.storage.TxStoreSignature(tx, cred); err != nil {
return err
}
return client.storage.TxStoreAttributes(tx, id, client.attributes[id])
})
}
func generateSecretKey() (*secretKey, error) {
......@@ -261,7 +272,7 @@ func generateSecretKey() (*secretKey, error) {
// Removal methods
func (client *Client) remove(id irma.CredentialTypeIdentifier, index int, storenow bool) error {
func (client *Client) remove(id irma.CredentialTypeIdentifier, index int, storeLog bool) error {
// Remove attributes
list, exists := client.attributes[id]
if !exists || index >= len(list) {
......@@ -269,35 +280,37 @@ func (client *Client) remove(id irma.CredentialTypeIdentifier, index int, storen
}
attrs := list[index]
client.attributes[id] = append(list[:index], list[index+1:]...)
if storenow {
if err := client.storage.StoreAttributes(client.attributes); err != nil {
removed := map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
removed[id] = attrs.Strings()
err := client.storage.Transaction(func(tx *transaction) error {
if err := client.storage.TxDeleteSignature(tx, attrs); err != nil {
return err
}
if err := client.storage.TxStoreAttributes(tx, id, client.attributes[id]); err != nil {
return err
}
if storeLog {
return client.storage.TxAddLogEntry(tx, &LogEntry{
Type: ActionRemoval,
Time: irma.Timestamp(time.Now()),
Removed: removed,
})
}
return nil
})
if err != nil {
return err
}
// Remove credential
// Remove credential from cache
if creds, exists := client.credentialsCache[id]; exists {
if _, exists := creds[index]; exists {
delete(creds, index)
client.credentialsCache[id] = creds
}
}
// Remove signature from storage
if err := client.storage.DeleteSignature(attrs); err != nil {
return err
}
removed := map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
removed[id] = attrs.Strings()
if storenow {
return client.storage.AddLogEntry(&LogEntry{
Type: ActionRemoval,
Time: irma.Timestamp(time.Now()),
Removed: removed,
})
}
return nil
}
......@@ -326,20 +339,25 @@ func (client *Client) RemoveAllCredentials() error {
if attrs.CredentialType() != nil {
removed[attrs.CredentialType().Identifier()] = attrs.Strings()
}
_ = client.storage.DeleteSignature(attrs)
}
}
client.attributes = map[irma.CredentialTypeIdentifier][]*irma.AttributeList{}
if err := client.storage.StoreAttributes(client.attributes); err != nil {
return err
}
logentry := &LogEntry{
Type: ActionRemoval,
Time: irma.Timestamp(time.Now()),
Removed: removed,
}
return client.storage.AddLogEntry(logentry)
return client.storage.Transaction(func(tx *transaction) error {
if err := client.storage.TxDeleteAllAttributes(tx); err != nil {
return err
}
if err := client.storage.TxDeleteAllSignatures(tx); err != nil {
return err
}
return client.storage.TxAddLogEntry(tx, logentry)
})
}
// Attribute and credential getter methods
......@@ -804,7 +822,7 @@ func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, re
if err != nil {
return err
}
if err = client.addCredential(newcred, true); err != nil {
if err = client.addCredential(newcred); err != nil {
return err
}
}
......@@ -1022,6 +1040,9 @@ func (client *Client) ConfigurationUpdated(downloaded *irma.IrmaIdentifierSet) e
attrs[j] = big.NewInt(0)
}
client.attributes[id][i].Ints = attrs
if err := client.storage.StoreAttributes(id, client.attributes[id]); err != nil {
return err
}
if _, contains = client.credentialsCache[id]; !contains {
continue
......@@ -1037,5 +1058,5 @@ func (client *Client) ConfigurationUpdated(downloaded *irma.IrmaIdentifierSet) e
}
}
return client.storage.StoreAttributes(client.attributes)
return nil
}
package irmaclient
import (
"github.com/privacybydesign/irmago/internal/test"
"testing"
)
func TestConvertingLegacyStorage(t *testing.T) {
test.SetTestStorageDir("legacy_teststorage")
defer test.SetTestStorageDir("teststorage")
// Test all tests in this file with legacy storage too
t.Run("TestVerify", TestVerify)
t.Run("TestStorageDeserialization", TestStorageDeserialization)
t.Run("TestCandidates", TestCandidates)
t.Run("TestCandidateConjunctionOrder", TestCandidateConjunctionOrder)
t.Run("TestCredentialRemoval", TestCredentialRemoval)
t.Run("TestWrongSchemeManager", TestWrongSchemeManager)
t.Run("TestCredentialInfoListNewAttribute", TestCredentialInfoListNewAttribute)
// TestFreshStorage is not needed, because this test does not use an existing storage
t.Run("TestKeyshareEnrollmentRemoval", TestKeyshareEnrollmentRemoval)
t.Run("TestUpdatePreferences", TestUpdatePreferences)
t.Run("TestUpdatingStorage", TestUpdatingStorage)
}
......@@ -3,6 +3,7 @@ package irmaclient
import (
"encoding/json"
"errors"
"github.com/privacybydesign/irmago/internal/fs"
"os"
"path/filepath"
"testing"
......@@ -10,7 +11,6 @@ import (
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs"
"github.com/privacybydesign/irmago/internal/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
......@@ -29,8 +29,10 @@ func TestMain(m *testing.M) {
func parseStorage(t *testing.T) *Client {
test.SetupTestStorage(t)
require.NoError(t, fs.CopyDirectory(filepath.Join("..", "testdata", "teststorage"),
filepath.Join("..", "testdata", "storage", "test")))
return parseExistingStorage(t)
}
func parseExistingStorage(t *testing.T) *Client {
client, err := New(
filepath.Join("..", "testdata", "storage", "test"),
filepath.Join("..", "testdata", "irma_configuration"),
......@@ -253,6 +255,14 @@ func TestCredentialRemoval(t *testing.T) {
cred, err = client.credential(id2, 0)
require.NoError(t, err)
require.Nil(t, cred)
// Also check whether credential is removed after reloading the storage
err = client.storage.db.Close()
require.NoError(t, err)
client = parseExistingStorage(t)
cred, err = client.credential(id2, 0)
require.NoError(t, err)
require.Nil(t, cred)
}
func TestWrongSchemeManager(t *testing.T) {
......@@ -298,6 +308,59 @@ func TestCredentialInfoListNewAttribute(t *testing.T) {
require.Fail(t, "studentCard credential not found")
}
func TestFreshStorage(t *testing.T) {
test.CreateTestStorage(t)
defer test.ClearTestStorage(t)
path := filepath.Join(test.FindTestdataFolder(t), "storage", "test")
err := fs.EnsureDirectoryExists(path)
require.NoError(t, err)
client := parseExistingStorage(t)
require.NoError(t, err)
require.NotNil(t, client)
}
func TestKeyshareEnrollmentRemoval(t *testing.T) {
client := parseStorage(t)
defer test.ClearTestStorage(t)
err := client.KeyshareRemove(irma.NewSchemeManagerIdentifier("test"))
require.NoError(t, err)
err = client.storage.db.Close()
require.NoError(t, err)
client = parseExistingStorage(t)
require.NotContains(t, client.keyshareServers, "test")
}
func TestUpdatePreferences(t *testing.T) {
client := parseStorage(t)
defer test.ClearTestStorage(t)
client.SetCrashReportingPreference(!defaultPreferences.EnableCrashReporting)
client.applyPreferences()
err := client.storage.db.Close()
require.NoError(t, err)
client = parseExistingStorage(t)
require.NoError(t, err)
require.Equal(t, false, client.Preferences.EnableCrashReporting)
}
func TestUpdatingStorage(t *testing.T) {
client := parseStorage(t)
defer test.ClearTestStorage(t)
require.NotNil(t, client)
// Check whether all update functions succeeded
for _, u := range client.updates {
require.Equal(t, true, u.Success)
}
}
// ------
type TestClientHandler struct {
......
package irmaclient
import (
"encoding/json"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/fs"
"io/ioutil"
"path/filepath"
)
// This file contains the legacy storage based on files. These functions are needed
// in the upgrade path to convert the file based storage to the bbolt based storage.
// The new storage functions for bbolt can be found in storage.go.
type fileStorage struct {
storagePath string
Configuration *irma.Configuration
}
// Legacy filenames in which we stored stuff
const (
skFile = "sk"
attributesFile = "attrs"
kssFile = "kss"
updatesFile = "updates"
logsFile = "logs"
preferencesFile = "preferences"
signaturesDir = "sigs"
)
func (f *fileStorage) path(p string) string {
return filepath.Join(f.storagePath, p)
}
func (f *fileStorage) load(dest interface{}, path string) (err error) {
exists, err := fs.PathExists(f.path(path))
if err != nil || !exists {
return
}
bytes, err := ioutil.ReadFile(f.path(path))
if err != nil {
return
}
return json.Unmarshal(bytes, dest)
}
func (f *fileStorage) signatureFilename(attrs *irma.AttributeList) string {
// We take the SHA256 hash over all attributes as the filename for the signature.
// This means that the signatures of two credentials that have identical attributes
// will be written to the same file, one overwriting the other - but that doesn't
// matter, because either one of the signatures is valid over both attribute lists,
// so keeping one of them suffices.
return filepath.Join(signaturesDir, attrs.Hash())
}
func (f *fileStorage) LoadSignature(attrs *irma.AttributeList) (signature *gabi.CLSignature, err error) {
sigpath := f.signatureFilename(attrs)
if err := fs.AssertPathExists(f.path(sigpath)); err != nil {
return nil, err
}
signature = new(gabi.CLSignature)
if err := f.load(signature, sigpath); err != nil {
return nil, err
}
return signature, nil
}
// LoadSecretKey retrieves and returns the secret key from file storage. When no secret key
// file is found, nil is returned.
func (f *fileStorage) LoadSecretKey() (*secretKey, error) {
var err error
sk := &secretKey{}
if err = f.load(sk, skFile); err != nil {
return nil, err
}
if sk.Key != nil {
return sk, nil
}
return nil, nil
}
func (f *fileStorage) LoadAttributes() (list map[irma.CredentialTypeIdentifier][]*irma.AttributeList, err error) {
// The attributes are stored as a list of instances of AttributeList
temp := []*irma.AttributeList{}
if err = f.load(&temp, attributesFile); err != nil {
return
}
list = make(map[irma.CredentialTypeIdentifier][]*irma.AttributeList)
for _, attrlist := range temp {
attrlist.MetadataAttribute = irma.MetadataFromInt(attrlist.Ints[0], f.Configuration)
id := attrlist.CredentialType()
var ct irma.CredentialTypeIdentifier
if id != nil {
ct = id.Identifier()
}
if _, contains := list[ct]; !contains {
list[ct] = []*irma.AttributeList{}
}
list[ct] = append(list[ct], attrlist)
}
return list, nil
}
func (f *fileStorage) LoadKeyshareServers() (ksses map[irma.SchemeManagerIdentifier]*keyshareServer, err error) {
ksses = make(map[irma.SchemeManagerIdentifier]*keyshareServer)
if err := f.load(&ksses, kssFile); err != nil {
return nil, err
}
return ksses, nil
}
func (f *fileStorage) LoadUpdates() (updates []update, err error) {
updates = []update{}
if err := f.load(&updates, updatesFile); err != nil {
return nil, err
}
return updates, nil
}
func (f *fileStorage) LoadPreferences() (Preferences, error) {
config := defaultPreferences
return config, f.load(&config, preferencesFile)
}
func (f *fileStorage) LoadLogs() (logs []*LogEntry, err error) {
return logs, f.load(&logs, logsFile)
}
This diff is collapsed.
......@@ -4,8 +4,9 @@ import (
"encoding/json"
"time"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/irmago"
"go.etcd.io/bbolt"
)
// This file contains the update mechanism for Client
......@@ -42,18 +43,15 @@ var clientUpdates = []func(client *Client) error{
// 6: Remove earlier log items of wrong format
nil, // No longer necessary
// 7: Concert log entries to bbolt database
// 7: Convert log entries to bbolt database
func(client *Client) error {
var logs []*LogEntry
var err error
// If loading logs fails, the logs file is likely empty and then there are no log entries to update
// Therefore throw away the error and mark the update as successful.
if err = client.storage.load(&logs, logsFile); err != nil {
logs, err := client.fileStorage.LoadLogs()
if err != nil {
return nil
}
// Open one bolt transaction to process all our log entries in
err = client.storage.db.Update(func(tx *bbolt.Tx) error {
err = client.storage.Transaction(func(tx *transaction) error {
for _, log := range logs {
// As log.Request is a json.RawMessage it would not get updated to the new session request
// format by re-marshaling the containing struct, as normal struct members would,
......@@ -74,6 +72,78 @@ var clientUpdates = []func(client *Client) error{
})
return err
},
// 8: Move other user storage to bbolt database
func(client *Client) error {
sk, err := client.fileStorage.LoadSecretKey()
if err != nil {
return err
}
// When no secret key is found, it means the storage is fresh. No update is needed.
if sk == nil {
return nil
}
attrs, err := client.fileStorage.LoadAttributes()
if err != nil {
return err
}
sigs := make(map[string]*gabi.CLSignature)
for _, attrlistlist := range attrs {
for _, attrlist := range attrlistlist {
sig, err := client.fileStorage.LoadSignature(attrlist)
if err != nil {
return err
}
sigs[attrlist.Hash()] = sig
}
}
ksses, err := client.fileStorage.LoadKeyshareServers()
if err != nil {
return err
}
prefs, err := client.fileStorage.LoadPreferences()
if err != nil {
return err
}
// Preferences are already loaded in client, refresh
client.Preferences = prefs
client.applyPreferences()
updates, err := client.fileStorage.LoadUpdates()
if err != nil {
return err
}
return client.storage.Transaction(func(tx *transaction) error {
if err = client.storage.TxStoreSecretKey(tx, sk); err != nil {
return err
}
for credTypeID, attrslistlist := range attrs {
if err = client.storage.TxStoreAttributes(tx, credTypeID, attrslistlist); err != nil {
return err
}
}
for hash, sig := range sigs {
err = client.storage.TxStoreCLSignature(tx, hash, sig)
if err != nil {
return err
}
}
if err = client.storage.TxStoreKeyshareServers(tx, ksses); err != nil {
return err
}
if err = client.storage.TxStorePreferences(tx, prefs); err != nil {
return err
}
return client.storage.TxStoreUpdates(tx, updates)
})
},
}
// update performs any function from clientUpdates that has not
......@@ -85,6 +155,18 @@ func (client *Client) update() error {
if client.updates, err = client.storage.LoadUpdates(); err != nil {
return err
}
// When no updates are found, it can either be a fresh storage or the storage has not been updated
// to bbolt yet. Therefore also check the updates file.
if len(client.updates) == 0 {
if client.updates, err = client.fileStorage.LoadUpdates(); err != nil {
return err
}
}
// Early exit if all updates are already performed to prevent superfluously storing the updates array
if len(client.updates) == len(clientUpdates) {
return nil
}
// Perform all new updates
for i := len(client.updates); i < len(clientUpdates); i++ {
......
This diff is collapsed.
......@@ -193,6 +193,8 @@ const (
ErrorServerResponse = ErrorType("serverResponse")
// Credential type not present in our Configuration
ErrorUnknownIdentifier = ErrorType("unknownIdentifier")
// Non-optional attribute not present in credential
ErrorRequiredAttributeMissing = ErrorType("requiredAttributeMissing")
// Error during downloading of credential type, issuer, or public keys
ErrorConfigurationDownload = ErrorType("configurationDownload")
// IRMA requests refers to unknown scheme manager
......
......@@ -417,6 +417,7 @@ func (dr *DisclosureRequest) identifiers() *IrmaIdentifierSet {
Issuers: map[IssuerIdentifier]struct{}{},
CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
PublicKeys: map[IssuerIdentifier][]int{},
AttributeTypes: map[AttributeTypeIdentifier]struct{}{},
}
_ = dr.Disclose.Iterate(func(a *AttributeRequest) error {
......@@ -424,6 +425,7 @@ func (dr *DisclosureRequest) identifiers() *IrmaIdentifierSet {
ids.SchemeManagers[attr.CredentialTypeIdentifier().IssuerIdentifier().SchemeManagerIdentifier()] = struct{}{}
ids.Issuers[attr.CredentialTypeIdentifier().IssuerIdentifier()] = struct{}{}
ids.CredentialTypes[attr.CredentialTypeIdentifier()] = struct{}{}
ids.AttributeTypes[attr] = struct{}{}
return nil
})
......@@ -487,7 +489,7 @@ func (cr *CredentialRequest) Validate(conf *Configuration) error {
}
}
if !found {
return errors.New("Credential request contaiins unknown attribute")
return errors.New("Credential request contains unknown attribute")
}
}
......@@ -540,6 +542,7 @@ func (ir *IssuanceRequest) Identifiers() *IrmaIdentifierSet {
SchemeManagers: map[SchemeManagerIdentifier]struct{}{},
Issuers: map[IssuerIdentifier]struct{}{},
CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
AttributeTypes: map[AttributeTypeIdentifier]struct{}{},
PublicKeys: map[IssuerIdentifier][]int{},
}
......@@ -547,7 +550,11 @@ func (ir *IssuanceRequest) Identifiers() *IrmaIdentifierSet {
issuer := credreq.CredentialTypeID.IssuerIdentifier()
ir.ids.SchemeManagers[issuer.SchemeManagerIdentifier()] = struct{}{}
ir.ids.Issuers[issuer] = struct{}{}
ir.ids.CredentialTypes[credreq.CredentialTypeID] = struct{}{}
credID := credreq.CredentialTypeID
ir.ids.CredentialTypes[credID] = struct{}{}
for attr, _ := range credreq.Attributes { // this is kind of ugly
ir.ids.AttributeTypes[NewAttributeTypeIdentifier(credID.String()+"."+attr)] = struct{}{}
}
if ir.ids.PublicKeys[issuer] == nil {
ir.ids.PublicKeys[issuer] = []int{}
}
......
......@@ -195,12 +195,12 @@ func jwtAuthenticate(
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
}
if !claims.VerifyIssuedAt(time.Now().Unix(), true) {
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt not yet valid")
}
if time.Unix(claims.IssuedAt, 0).Add(time.Duration(maxRequestAge) * time.Second).Before(time.Now()) {
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt too old")
}
if !claims.VerifyIssuedAt(time.Now().Unix(), true) {
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt not yet valid")
}
// Read JWT contents
parsedJwt, err := irma.ParseRequestorJwt(claims.Subject, requestorJwt)
......
package requestorserver
import (
"encoding/json"
"github.com/dgrijalva/jwt-go"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestPresharedKeyAuthenticator_Authenticate(t *testing.T) {
authenticator := PresharedKeyAuthenticator{presharedkeys: map[string]string{
"token": "my_requestor",
}}
validRequestBody := []byte(`{"request": {"@context":"https://irma.app/ld/request/disclosure/v2","disclose":[[["irma-demo.RU.studentCard.studentID"]]]}}`)
t.Run("valid", func(t *testing.T) {
requestHeaders := map[string][]string{
"Authorization": {"token"},
"Content-Type": {"application/json"},
}
applies, parsedRequest, requestor, err := authenticator.Authenticate(requestHeaders, validRequestBody)
if err != nil {
require.NoError(t, err)
}
require.True(t, applies)
require.Equal(t, "irma-demo.RU.studentCard.studentID", parsedRequest.SessionRequest().Disclosure().Disclose[0][0][0].Type.String())
require.Equal(t, "my_requestor", requestor)
})
// tests below here will give warnings
server.Logger.SetLevel(logrus.ErrorLevel)
t.Run("invalid content", func(t *testing.T) {
requestHeaders := map[string][]string{
"Authorization": {"token"},
"Content-Type": {"application/json"},
}
invalidRequestBody := []byte(`{}`)
applies, _, _, err := authenticator.Authenticate(requestHeaders, invalidRequestBody)
require.Error(t, err)
require.True(t, applies)
})
t.Run("invalid token", func(t *testing.T) {
requestHeaders := map[string][]string{
"Authorization": {"invalid"},
"Content-Type": {"application/json"},
}
applies, _, _, err := authenticator.Authenticate(requestHeaders, validRequestBody)
require.True(t, applies)
require.Error(t, err)
})
t.Run("no authorization header", func(t *testing.T) {
requestHeaders := map[string][]string{
"UnusedHeader": {"token"},
"Content-Type": {"application/json"},
}
applies, _, _, err := authenticator.Authenticate(requestHeaders, validRequestBody)
require.False(t, applies)
if err != nil {
require.NoError(t, err)
}
})
t.Run("without content type", func(t *testing.T) {
requestHeaders := map[string][]string{
"Authorization": {"token"},
}
applies, _, _, err := authenticator.Authenticate(requestHeaders, validRequestBody)
require.False(t, applies)
if err != nil {
require.NoError(t, err)
}
})
}
func TestHmacAuthenticator_Authenticate(t *testing.T) {
key := []byte("953BCAB6F25F3622619A9A16BE895")
invalidKey := []byte("A5BB219FFB6199756DF8A284A3392")
authenticator := HmacAuthenticator{
hmackeys: map[string]interface{}{
"my_requestor": key,
},
maxRequestAge: 500,
}
disclosureRequestData := `{"@context":"https://irma.app/ld/request/disclosure/v2","disclose":[[["irma-demo.RU.studentCard.studentID"]]]}`
disclosureRequest := &irma.DisclosureRequest{}
require.NoError(t, json.Unmarshal([]byte(disclosureRequestData), disclosureRequest))
j := irma.NewServiceProviderJwt("my_requestor", disclosureRequest)
validJwtData, jErr := j.Sign(jwt.SigningMethodHS256, key)
require.NoError(t, jErr)
requestHeaders := map[string][]string{
"Content-Type": {"text/plain"},
}
t.Run("valid", func(t *testing.T) {
applies, parsedRequest, requestor, err := authenticator.Authenticate(requestHeaders, []byte(validJwtData))
if err != nil {
require.NoError(t, err)
}
require.True(t, applies)
require.Equal(t, "irma-demo.RU.studentCard.studentID", parsedRequest.SessionRequest().Disclosure().Disclose[0][0][0].Type.String())
require.Equal(t, "my_requestor", requestor)
})
server.Logger.SetLevel(logrus.ErrorLevel)
t.Run("invalid jwt requestor", func(t *testing.T) {
j := irma.NewServiceProviderJwt("another_requestor", disclosureRequest)
invalidJwtData, jErr := j.Sign(jwt.SigningMethodHS256, key)
require.NoError(t, jErr)
applies, _, _, err := authenticator.Authenticate(requestHeaders, []byte(invalidJwtData))
require.True(t, applies)
require.Error(t, err)
})
t.Run("empty jwt data", func(t *testing.T) {
claims := (*jwt.MapClaims)(&map[string]interface{}{
"sub": "verification_request",
"iss": "my_requestor",
"iat": time.Now().Unix(),
"sprequest": map[string]interface{}{},
})
emptyJwtData, jErr := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(key)
require.NoError(t, jErr)
applies, _, _, err := authenticator.Authenticate(requestHeaders, []byte(emptyJwtData))
require.True(t, applies)
require.Error(t, err)
require.Equal(t, string(server.ErrorInvalidRequest.Type), err.ErrorName)
})
t.Run("old jwt data", func(t *testing.T) {
j := irma.NewServiceProviderJwt("my_requestor", disclosureRequest)
j.IssuedAt = (irma.Timestamp)(time.Unix(0, 0))
invalidJwtData, jErr := j.Sign(jwt.SigningMethodHS256, key)
require.NoError(t, jErr)
applies, _, _, err := authenticator.Authenticate(requestHeaders, []byte(invalidJwtData))
require.True(t, applies)
require.Error(t, err)
require.Equal(t, string(server.ErrorUnauthorized.Type), err.ErrorName)
})
t.Run("jwt data not yet valid", func(t *testing.T) {
j := irma.NewServiceProviderJwt("my_requestor", disclosureRequest)
j.IssuedAt = (irma.Timestamp)(time.Now().AddDate(1, 0, 0))
invalidJwtData, jErr := j.Sign(jwt.SigningMethodHS256, key)
require.NoError(t, jErr)
applies, _, _, err := authenticator.Authenticate(requestHeaders, []byte(invalidJwtData))
require.True(t, applies)
require.Error(t, err)
require.Equal(t, string(server.ErrorInvalidRequest.Type), err.ErrorName)
})
t.Run("jwt signed using invalid key", func(t *testing.T) {
j := irma.NewServiceProviderJwt("my_requestor", disclosureRequest)
invalidJwtData, jErr := j.Sign(jwt.SigningMethodHS256, invalidKey)
require.NoError(t, jErr)
applies, _, _, err := authenticator.Authenticate(requestHeaders, []byte(invalidJwtData))
require.True(t, applies)
require.Error(t, err)
})
}
package requestorserver
import (
"encoding/json"
"testing"
"time"
irma "github.com/privacybydesign/irmago"
"github.com/stretchr/testify/require"
)
func createCredentialRequest(identifier string, attributes map[string]string) []*irma.CredentialRequest {
expiry := irma.Timestamp(irma.FloorToEpochBoundary(time.Now().AddDate(1, 0, 0)))
return []*irma.CredentialRequest{
{
Validity: &expiry,
CredentialTypeID: irma.NewCredentialTypeIdentifier(identifier),
Attributes: attributes,
},
}
}
func TestCanIssue(t *testing.T) {
confJSON := `{
"requestors": {
"myapp": {
"disclose_perms": [ "irma-demo.MijnOverheid.ageLower.over18" ],
"sign_perms": [ "irma-demo.MijnOverheid.ageLower.*" ],
"issue_perms": [ "irma-demo.MijnOverheid.ageLower" ],
"auth_method": "token",
"key": "eGE2PSomOT84amVVdTU"
}
}
}`
t.Run("allowed credential request", func(t *testing.T) {
var conf Configuration
require.NoError(t, json.Unmarshal([]byte(confJSON), &conf))
credentialRequest := createCredentialRequest("irma-demo.MijnOverheid.ageLower", map[string]string{"over12": "yes"})
result, message := conf.CanIssue("myapp", credentialRequest)
require.True(t, result)
require.Empty(t, message)
})
t.Run("allowed credential request different attribute value", func(t *testing.T) {
var conf Configuration
require.NoError(t, json.Unmarshal([]byte(confJSON), &conf))
credentialRequest := createCredentialRequest("irma-demo.MijnOverheid.ageLower", map[string]string{"over16": "no"})
result, message := conf.CanIssue("myapp", credentialRequest)
require.True(t, result)
require.Empty(t, message)
})
t.Run("allowed credential request wrong requestor id", func(t *testing.T) {
var conf Configuration
require.NoError(t, json.Unmarshal([]byte(confJSON), &conf))
credentialRequest := createCredentialRequest("irma-demo.MijnOverheid.ageLower", map[string]string{"over12": "yes"})
result, message := conf.CanIssue("yourapp", credentialRequest)
require.False(t, result)
require.Empty(t, message)
})
t.Run("allowed credential request wrong credential identifier", func(t *testing.T) {
var conf Configuration
require.NoError(t, json.Unmarshal([]byte(confJSON), &conf))
credentialRequest := createCredentialRequest("irma-demo.MijnOverheid.ageUpper", map[string]string{"over12": "yes"})
result, message := conf.CanIssue("myapp", credentialRequest)
require.False(t, result)
require.Equal(t, "irma-demo.MijnOverheid.ageUpper", message)
})
t.Run("allowed credential request attribute wildcard", func(t *testing.T) {
var conf Configuration
require.NoError(t, json.Unmarshal([]byte(confJSON), &conf))
conf.Requestors["myapp"].Issuing[0] = "irma-demo.MijnOverheid.*"
credentialRequest := createCredentialRequest("irma-demo.MijnOverheid.ageLower", map[string]string{"over12": "yes"})
result, message := conf.CanIssue("myapp", credentialRequest)
require.True(t, result)
require.Empty(t, message)
})
t.Run("allowed credential request issuer wildcard", func(t *testing.T) {
var conf Configuration
require.NoError(t, json.Unmarshal([]byte(confJSON), &conf))
conf.Requestors["myapp"].Issuing[0] = "irma-demo.*"
credentialRequest := createCredentialRequest("irma-demo.MijnOverheid.ageLower", map[string]string{"over12": "yes"})
result, message := conf.CanIssue("myapp", credentialRequest)
require.True(t, result)
require.Empty(t, message)
})
t.Run("allowed credential request single wildcard", func(t *testing.T) {
var conf Configuration
require.NoError(t, json.Unmarshal([]byte(confJSON), &conf))
conf.Requestors["myapp"].Issuing[0] = "*"
credentialRequest := createCredentialRequest("irma-demo.MijnOverheid.ageLower", map[string]string{"over12": "yes"})
result, message := conf.CanIssue("myapp", credentialRequest)
require.True(t, result)
require.Empty(t, message)
})
}
func createAttributesConDisCon(identifier string) irma.AttributeConDisCon {
return irma.AttributeConDisCon{
{{irma.NewAttributeRequest(identifier)}},
}
}
func TestCanVerifyOrSign(t *testing.T) {
confJSON := `{
"requestors": {
"myapp": {
"disclose_perms": [ "irma-demo.MijnOverheid.ageLower.over18" ],
"sign_perms": [ "irma-demo.MijnOverheid.ageLower.over18" ],
"issue_perms": [ "irma-demo.MijnOverheid.ageLower" ],
"auth_method": "token",
"key": "eGE2PSomOT84amVVdTU"
}
}
}`
var disclosingCases = []struct {
description string
attributeConDisCon string
disclosePerm string
requestorName string
result bool
message string
}{
{
"allowed disclosing request",
"irma-demo.MijnOverheid.ageLower.over18",
"irma-demo.MijnOverheid.ageLower.over18",
"myapp",
true,
"",
},
{
"allowed disclosing request incorrect requestor",
"irma-demo.MijnOverheid.ageLower.over18",
"irma-demo.MijnOverheid.ageLower.over18",
"yourapp",
false,
"",
},
{
"allowed disclosing request incorrect attribute",
"irma-demo.MijnOverheid.ageLower.over16",
"irma-demo.MijnOverheid.ageLower.over18",
"myapp",
false,
"irma-demo.MijnOverheid.ageLower.over16",
},
{
"allowed disclosing single wildcard",
"irma-demo.MijnOverheid.ageLower.over18",
"*",
"myapp",
true,
"",
},
{
"allowed disclosing request correct issuer wildcard",
"irma-demo.MijnOverheid.ageLower.over18",
"irma-demo.*",
"myapp",
true,
"",
},
{