Unverified Commit f71c2a83 authored by Sietse Ringers's avatar Sietse Ringers Committed by GitHub
Browse files

Merge pull request #10 from koen92/verify-signatures

Verify signatures
parents c10536c1 541df010
......@@ -47,7 +47,7 @@
branch = "master"
name = "github.com/mhe/gabi"
packages = ["."]
revision = "6123a11fd5206d74c0f01cd1d60a70d96bab366b"
revision = "bf5016a5f3005bef1d2416989588cefcf9a73cb7"
[[projects]]
name = "github.com/pkg/errors"
......
......@@ -8,6 +8,7 @@ import (
"math/big"
"time"
"fmt"
"github.com/go-errors/errors"
"github.com/mhe/gabi"
)
......@@ -18,6 +19,24 @@ const (
metadataLength = 1 + 3 + 2 + 2 + 16
)
type AttributeResult struct {
AttributeValue string `json:"value"` // Value of the disclosed attribute
AttributeId AttributeTypeIdentifier `json:"id"`
AttributeProofStatus AttributeProofStatus `json:"status"`
}
type AttributeResultList []*AttributeResult
// AttributeProofStatus is the proof status of a single attribute
type AttributeProofStatus string
const (
PRESENT = AttributeProofStatus("PRESENT") // Attribute is disclosed and matches the value
EXTRA = AttributeProofStatus("EXTRA") // Attribute is disclosed, but wasn't requested in request
MISSING = AttributeProofStatus("MISSING") // Attribute is NOT disclosed, but should be according to request
INVALID_VALUE = AttributeProofStatus("INVALID_VALUE") // Attribute is disclosed, but has invalid value according to request
)
var (
metadataVersion = []byte{0x02}
......@@ -34,7 +53,7 @@ type metadataField struct {
offset int
}
// MetadataAttribute represent a metadata attribute. Contains the credential type, signing date, validity, and the public key counter.
// metadataAttribute represents a metadata attribute. Contains the credential type, signing date, validity, and the public key counter.
type MetadataAttribute struct {
Int *big.Int
pk *gabi.PublicKey
......@@ -287,9 +306,28 @@ type AttributeDisjunction struct {
selected *AttributeTypeIdentifier
}
// AttributeDisjunction with the disclosed value that is used to satisfy the disjunction
type DisclosedAttributeDisjunction struct {
AttributeDisjunction
DisclosedValue string
DisclosedId AttributeTypeIdentifier
ProofStatus AttributeProofStatus
}
// An AttributeDisjunctionList is a list of AttributeDisjunctions.
type AttributeDisjunctionList []*AttributeDisjunction
// Convert disjunction to a DisclosedAttributeDisjunction that contains disclosed attribute+value
func (disjunction *AttributeDisjunction) ToDisclosedAttributeDisjunction(ar *AttributeResult) *DisclosedAttributeDisjunction {
return &DisclosedAttributeDisjunction{
AttributeDisjunction: *disjunction,
DisclosedValue: ar.AttributeValue,
DisclosedId: ar.AttributeId,
ProofStatus: ar.AttributeProofStatus,
}
}
// HasValues indicates if the attributes of this disjunction have values
// that should be satisfied.
func (disjunction *AttributeDisjunction) HasValues() bool {
......@@ -310,6 +348,26 @@ func (disjunction *AttributeDisjunction) Satisfied() bool {
return false
}
// Check whether specified attributedisjunction satisfy a list of disclosed attributes
// We return true if one of the attributes in the disjunction is satisfied
func (disjunction *AttributeDisjunction) SatisfyDisclosed(disclosed DisclosedCredentialList, conf *Configuration) (bool, *DisclosedAttributeDisjunction) {
var attributeResult *AttributeResult
for _, attr := range disjunction.Attributes {
requestedValue := disjunction.Values[attr]
var isSatisfied bool
isSatisfied, attributeResult = disclosed.isAttributeSatisfied(attr, requestedValue)
if isSatisfied {
return true, disjunction.ToDisclosedAttributeDisjunction(attributeResult)
}
}
// Nothing satisfied, attributeResult will contain the last attribute of the original request
// TODO: do we want this?
return false, disjunction.ToDisclosedAttributeDisjunction(attributeResult)
}
// MatchesConfig returns true if all attributes contained in the disjunction are
// present in the specified configuration.
func (disjunction *AttributeDisjunction) MatchesConfig(conf *Configuration) bool {
......@@ -422,3 +480,19 @@ func (disjunction *AttributeDisjunction) UnmarshalJSON(bytes []byte) error {
return nil
}
func (al *AttributeResultList) String() string {
// TODO: pretty print?
str := "Attribute --- Value --- ProofStatus:"
for _, v := range *al {
str = str + "\n" + v.String()
}
return str
}
func (ar *AttributeResult) String() string {
return fmt.Sprintf("%v --- %v --- %v",
ar.AttributeId,
ar.AttributeValue,
ar.AttributeProofStatus)
}
......@@ -3,20 +3,21 @@ package irma
import (
"math/big"
"strings"
"time"
)
// CredentialInfo contains all information of an IRMA credential.
type CredentialInfo struct {
CredentialTypeID string // e.g., "irma-demo.RU.studentCard"
Name string // e.g., "studentCard"
IssuerID string // e.g., "RU"
SchemeManagerID string // e.g., "irma-demo"
Index int // This is the Index-th credential instance of this type
SignedOn Timestamp // Unix timestamp
Expires Timestamp // Unix timestamp
Attributes []TranslatedString // Human-readable rendered attributes
Logo string // Path to logo on storage
Hash string // SHA256 hash over the attributes
CredentialTypeID CredentialTypeIdentifier // e.g., "irma-demo.RU.studentCard"
Name string // e.g., "studentCard"
IssuerID IssuerIdentifier // e.g., "RU"
SchemeManagerID SchemeManagerIdentifier // e.g., "irma-demo"
Index int // This is the Index-th credential instance of this type
SignedOn Timestamp // Unix timestamp
Expires Timestamp // Unix timestamp
Attributes []TranslatedString // Human-readable rendered attributes
Logo string // Path to logo on storage
Hash string // SHA256 hash over the attributes
}
// A CredentialInfoList is a list of credentials (implements sort.Interface).
......@@ -29,27 +30,32 @@ func NewCredentialInfo(ints []*big.Int, conf *Configuration) *CredentialInfo {
return nil
}
attrs := make([]TranslatedString, len(credtype.Attributes))
for i := range credtype.Attributes {
val := string(ints[i+1].Bytes())
attrs[i] = TranslatedString(map[string]string{"en": val, "nl": val})
}
attrs := NewAttributeListFromInts(ints, conf)
id := credtype.Identifier()
issid := id.IssuerIdentifier()
return &CredentialInfo{
CredentialTypeID: id.String(),
CredentialTypeID: NewCredentialTypeIdentifier(id.String()),
Name: id.Name(),
IssuerID: issid.Name(),
SchemeManagerID: issid.SchemeManagerIdentifier().String(),
IssuerID: NewIssuerIdentifier(issid.Name()),
SchemeManagerID: NewSchemeManagerIdentifier(issid.SchemeManagerIdentifier().String()),
SignedOn: Timestamp(meta.SigningDate()),
Expires: Timestamp(meta.Expiry()),
Attributes: attrs,
Attributes: attrs.Strings(),
Logo: credtype.Logo(conf),
Hash: NewAttributeListFromInts(ints, conf).Hash(),
Hash: attrs.Hash(),
}
}
func (ci CredentialInfo) GetCredentialType(conf *Configuration) *CredentialType {
return conf.CredentialTypes[ci.CredentialTypeID]
}
// Returns true if credential is expired at moment of calling this function
func (ci CredentialInfo) IsExpired() bool {
return ci.Expires.Before(Timestamp(time.Now()))
}
// Len implements sort.Interface.
func (cl CredentialInfoList) Len() int {
return len(cl)
......
......@@ -81,6 +81,10 @@ type AttributeDescription struct {
Description TranslatedString
}
func (ad AttributeDescription) GetAttributeTypeIdentifier(cred CredentialTypeIdentifier) AttributeTypeIdentifier {
return NewAttributeTypeIdentifier(cred.String() + "." + ad.ID)
}
// IndexOf returns the index of the specified attribute if present,
// or an error (and -1) if not present.
func (ct CredentialType) IndexOf(ai AttributeTypeIdentifier) (int, error) {
......
package irmaclient
import (
"github.com/privacybydesign/irmago"
"github.com/mhe/gabi"
"github.com/privacybydesign/irmago"
)
// credential represents an IRMA credential, whose zeroth attribute
......
package irmaclient
import (
"encoding/json"
"fmt"
"testing"
"github.com/go-errors/errors"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test"
"testing"
)
type ManualSessionHandler struct {
permissionHandler PermissionHandler
pinHandler PinHandler
t *testing.T
c chan *irma.SessionError
errorChannel chan *irma.SessionError
resultChannel chan *irma.SignatureProofResult
sigRequest *irma.SignatureRequest // Request used to create signature
sigVerifyRequest *irma.SignatureRequest // Request used to verify signature
}
var client *Client
// Issue BSN credential using sessionHelper
func issue(t *testing.T, ms ManualSessionHandler) {
name := "testip"
jwtcontents := getIssuanceJwt(name, true)
sessionHandlerHelper(t, jwtcontents, "issue", client, &ms)
}
// Flip one bit in the proof string if invalidate is set to true
var invalidate bool
func corruptProofString(proof string) string {
if invalidate {
proofBytes := []byte(proof)
// 15 because this is somewhere in a bigint in the json string
proofBytes[15] ^= 0x01
return string(proofBytes)
}
return proof
}
// Create a ManualSessionHandler for unit tests
func createManualSessionHandler(request string, invalidRequest string, t *testing.T) ManualSessionHandler {
errorChannel := make(chan *irma.SessionError)
resultChannel := make(chan *irma.SignatureProofResult)
sigRequestJSON := []byte(request)
invalidSigRequestJSON := []byte(invalidRequest)
sigRequest := &irma.SignatureRequest{}
invalidSigRequest := &irma.SignatureRequest{}
json.Unmarshal(sigRequestJSON, sigRequest)
json.Unmarshal(invalidSigRequestJSON, invalidSigRequest)
return ManualSessionHandler{
t: t,
errorChannel: errorChannel,
resultChannel: resultChannel,
sigRequest: sigRequest,
sigVerifyRequest: invalidSigRequest,
}
}
func TestManualSession(t *testing.T) {
invalidate = false
request := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}"
ms := createManualSessionHandler(request, request, t)
client = parseStorage(t)
client.NewManualSession(request, &ms)
request := "{\"nonce\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}"
if err := <-ms.errorChannel; err != nil {
test.ClearTestStorage(t)
t.Fatal(*err)
}
channel := make(chan *irma.SessionError)
manualSessionHandler := ManualSessionHandler{t: t, c: channel}
// No errors, obtain proof result from channel
result := <-ms.resultChannel
if ps := result.ProofStatus; ps != irma.VALID {
t.Logf("Invalid proof result: %v Expected: %v", ps, irma.VALID)
t.Fail()
}
if attrStatus := result.ToAttributeResultList()[0].AttributeProofStatus; attrStatus != irma.PRESENT {
t.Logf("Invalid attribute result value: %v Expected: %v", attrStatus, irma.PRESENT)
t.Fail()
}
test.ClearTestStorage(t)
}
// Test if the session fails with unsatisfiable error if we cannot satify the signature request
func TestManualSessionUnsatisfiable(t *testing.T) {
invalidate = false
request := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":{\"irma-demo.RU.studentCard.studentID\": \"123\"}}]}"
ms := createManualSessionHandler(request, request, t)
client = parseStorage(t)
client.NewManualSession(request, &ms)
client.NewManualSession(request, &manualSessionHandler)
// Fail test if we won't get UnsatisfiableRequest error
if err := <-ms.errorChannel; err.ErrorType != irma.ErrorType("UnsatisfiableRequest") {
test.ClearTestStorage(t)
t.Fatal(*err)
}
test.ClearTestStorage(t)
}
// Test if proof verification fails with status 'ERROR_CRYPTO' if we verify it with an invalid nonce
func TestManualSessionInvalidNonce(t *testing.T) {
invalidate = false
request := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}"
invalidRequest := "{\"nonce\": 1, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}"
ms := createManualSessionHandler(request, invalidRequest, t)
client = parseStorage(t)
client.NewManualSession(request, &ms)
if err := <-ms.errorChannel; err != nil {
test.ClearTestStorage(t)
t.Fatal(*err)
}
// No errors, obtain proof result from channel
if result := <-ms.resultChannel; result.ProofStatus != irma.INVALID_CRYPTO {
t.Logf("Invalid proof result: %v Expected: %v", result.ProofStatus, irma.INVALID_CRYPTO)
t.Fail()
}
test.ClearTestStorage(t)
}
// Test if proof verification fails with status 'MISSING_ATTRIBUTES' if we provide it with a non-matching signature request
func TestManualSessionInvalidRequest(t *testing.T) {
invalidate = false
request := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}"
invalidRequest := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.university\"]}]}"
ms := createManualSessionHandler(request, invalidRequest, t)
client = parseStorage(t)
client.NewManualSession(request, &ms)
if err := <-ms.errorChannel; err != nil {
test.ClearTestStorage(t)
t.Fatal(*err)
}
// No errors, obtain proof result from channel
result := <-ms.resultChannel
if ps := result.ProofStatus; ps != irma.MISSING_ATTRIBUTES {
t.Logf("Invalid proof result: %v Expected: %v", ps, irma.MISSING_ATTRIBUTES)
t.Fail()
}
// First attribute result is MISSING, because it is in the request but not disclosed
if attrStatus := result.ToAttributeResultList()[0].AttributeProofStatus; attrStatus != irma.MISSING {
t.Logf("Invalid attribute result value: %v Expected: %v", attrStatus, irma.MISSING)
t.Fail()
}
// Second attribute result is EXTRA, since it is disclosed, but not matching the sigrequest
if attrStatus := result.ToAttributeResultList()[1].AttributeProofStatus; attrStatus != irma.EXTRA {
t.Logf("Invalid attribute result value: %v Expected: %v", attrStatus, irma.EXTRA)
t.Fail()
}
test.ClearTestStorage(t)
}
// Test if proof verification fails with status 'MISSING_ATTRIBUTES' if we provide it with invalid attribute values
func TestManualSessionInvalidAttributeValue(t *testing.T) {
invalidate = false
request := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":{\"irma-demo.RU.studentCard.studentID\": \"456\"}}]}"
invalidRequest := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":{\"irma-demo.RU.studentCard.studentID\": \"123\"}}]}"
ms := createManualSessionHandler(request, invalidRequest, t)
client = parseStorage(t)
client.NewManualSession(request, &ms)
if err := <-channel; err != nil {
if err := <-ms.errorChannel; err != nil {
test.ClearTestStorage(t)
t.Fatal(*err)
}
// No errors, obtain proof result from channel
result := <-ms.resultChannel
if ps := result.ProofStatus; ps != irma.MISSING_ATTRIBUTES {
t.Logf("Invalid proof result: %v Expected: %v", ps, irma.MISSING_ATTRIBUTES)
t.Fail()
}
if attrStatus := result.ToAttributeResultList()[0].AttributeProofStatus; attrStatus != irma.INVALID_VALUE {
t.Logf("Invalid attribute result value: %v Expected: %v", attrStatus, irma.INVALID_VALUE)
t.Fail()
}
test.ClearTestStorage(t)
}
func TestManualKeyShareSession(t *testing.T) {
invalidate = false
request := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"test.test.mijnirma.email\"]}]}"
ms := createManualSessionHandler(request, request, t)
client = parseStorage(t)
client.NewManualSession(request, &ms)
if err := <-ms.errorChannel; err != nil {
test.ClearTestStorage(t)
t.Fatal(*err)
}
// No errors, obtain proof result from channel
if result := <-ms.resultChannel; result.ProofStatus != irma.VALID {
t.Logf("Invalid proof result: %v Expected: %v", result.ProofStatus, irma.VALID)
t.Fail()
}
test.ClearTestStorage(t)
}
func TestManualSessionMultiProof(t *testing.T) {
invalidate = false
client = parseStorage(t)
// First, we need to issue an extra credential (BSN)
is := ManualSessionHandler{t: t, errorChannel: make(chan *irma.SessionError)}
go issue(t, is)
if err := <-is.errorChannel; err != nil {
fmt.Println("Error during initial issueing!")
t.Fatal(*err)
}
keyshareRequest := "{\"nonce\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"test.test.mijnirma.email\"]}]}"
// Request to sign with both BSN and StudentID
request := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]},{\"label\":\"BSN\",\"attributes\":[\"irma-demo.MijnOverheid.root.BSN\"]}]}"
channel := make(chan *irma.SessionError)
manualSessionHandler := ManualSessionHandler{t: t, c: channel}
ms := createManualSessionHandler(request, request, t)
client.NewManualSession(keyshareRequest, &manualSessionHandler)
client.NewManualSession(request, &ms)
if err := <-ms.errorChannel; err != nil {
test.ClearTestStorage(t)
t.Fatal(*err)
}
// No errors, obtain proof result from channel
result := <-ms.resultChannel
if ps := result.ProofStatus; ps != irma.VALID {
t.Logf("Invalid proof result: %v Expected: %v", result.ProofStatus, irma.VALID)
t.Fail()
}
if attrStatus := result.ToAttributeResultList()[0].AttributeProofStatus; attrStatus != irma.PRESENT {
t.Logf("Invalid attribute result value: %v Expected: %v", attrStatus, irma.PRESENT)
t.Fail()
}
if attrStatus := result.ToAttributeResultList()[1].AttributeProofStatus; attrStatus != irma.PRESENT {
t.Logf("Invalid attribute result value: %v Expected: %v", attrStatus, irma.PRESENT)
t.Fail()
}
test.ClearTestStorage(t)
}
if err := <-channel; err != nil {
func TestManualSessionInvalidProof(t *testing.T) {
invalidate = true
request := "{\"nonce\": 0, \"context\": 0, \"message\":\"I owe you everything\",\"messageType\":\"STRING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}"
ms := createManualSessionHandler(request, request, t)
client = parseStorage(t)
client.NewManualSession(request, &ms)
if err := <-ms.errorChannel; err != nil {
test.ClearTestStorage(t)
t.Fatal(*err)
}
// No errors, obtain proof result from channel
if result := <-ms.resultChannel; result.ProofStatus != irma.INVALID_CRYPTO {
t.Logf("Invalid proof result: %v Expected: %v", result.ProofStatus, irma.INVALID_CRYPTO)
t.Fail()
}
test.ClearTestStorage(t)
}
func (sh *ManualSessionHandler) Success(irmaAction irma.Action, result string) {
sh.c <- nil
switch irmaAction {
case irma.ActionSigning:
// Make proof corrupt if we want to test invalid proofs
result = corruptProofString(result)
go func() {
sh.resultChannel <- irma.VerifySig(client.Configuration, result, sh.sigVerifyRequest)
}()
}
sh.errorChannel <- nil
}
func (sh *ManualSessionHandler) UnsatisfiableRequest(irmaAction irma.Action, serverName string, missingAttributes irma.AttributeDisjunctionList) {
sh.t.Fail()
// This function is called from main thread, which blocks go channel, so need go routine here
go func() {
sh.errorChannel <- &irma.SessionError{
ErrorType: irma.ErrorType("UnsatisfiableRequest"),
}
}()
}
// Done in irma bridge?
func (sh *ManualSessionHandler) StatusUpdate(irmaAction irma.Action, status irma.Status) {}
func (sh *ManualSessionHandler) RequestPin(remainingAttempts int, ph PinHandler) {
ph(true, "12345")
}
func (sh *ManualSessionHandler) RequestSignaturePermission(request irma.SignatureRequest, requesterName string, ph PermissionHandler) {
c := irma.DisclosureChoice{request.Candidates[0]}
var attributes []*irma.AttributeIdentifier
for _, cand := range request.Candidates {
attributes = append(attributes, cand[0])
}
c := irma.DisclosureChoice{attributes}
ph(true, &c)
}
func (sh *ManualSessionHandler) RequestIssuancePermission(request irma.IssuanceRequest, issuerName string, ph PermissionHandler) {
ph(true, nil)
}
// These handlers should not be called, fail test if they are called
func (sh *ManualSessionHandler) Cancelled(irmaAction irma.Action) {
sh.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.New("Session was cancelled")})
sh.errorChannel <- &irma.SessionError{Err: errors.New("Session was cancelled")}
}
func (sh *ManualSessionHandler) KeyshareEnrollmentMissing(manager irma.SchemeManagerIdentifier) {
sh.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.Errorf("Missing keyshare server %s", manager.String())})
}
func (sh *ManualSessionHandler) RequestIssuancePermission(request irma.IssuanceRequest, issuerName string, ph PermissionHandler) {
sh.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.New("Unexpected session type")})
func (sh *ManualSessionHandler) MissingKeyshareEnrollment(manager irma.SchemeManagerIdentifier) {
sh.errorChannel <- &irma.SessionError{Err: errors.Errorf("Missing keyshare server %s", manager.String())}
}
func (sh *ManualSessionHandler) RequestSchemeManagerPermission(manager *irma.SchemeManager, callback func(proceed bool)) {
sh.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.New("Unexpected session type")})
sh.errorChannel <- &irma.SessionError{Err: errors.New("Unexpected session type")}
}
func (sh *ManualSessionHandler) RequestVerificationPermission(request irma.DisclosureRequest, verifierName string, ph PermissionHandler) {
sh.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.New("Unexpected session type")})
sh.errorChannel <- &irma.SessionError{Err: errors.New("Unexpected session type")}
}
func (sh *ManualSessionHandler) Failure(irmaAction irma.Action, err *irma.SessionError) {
fmt.Println(err.Err)
select {
case sh.c <- err:
// nop
default:
sh.t.Fatal(err)
}
sh.errorChannel <- err
}
func (sh *ManualSessionHandler) KeyshareBlocked(manager irma.SchemeManagerIdentifier, duration int) {
sh.Failure(irma.ActionUnknown, &irma.SessionError{Err: errors.New("KeyshareBlocked")})
sh.error