Commit fc7bade6 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

feat: support issuance during disclosure in irmaclient

parent 2b18689d
......@@ -22,6 +22,11 @@ var Logger *logrus.Logger
// Only for use in unit tests.
var ForceHTTPS = true
const (
sessionChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
sessionTokenLength = 20
)
// AssertPathExists returns nil only if it has been successfully
// verified that all specified paths exists.
func AssertPathExists(paths ...string) error {
......@@ -264,3 +269,17 @@ func RandomBigInt(limit *big.Int) *big.Int {
type SSECtx struct {
Component, Arg string
}
func NewSessionToken() string {
r := make([]byte, sessionTokenLength)
_, err := rand.Read(r)
if err != nil {
panic(err)
}
b := make([]byte, sessionTokenLength)
for i := range b {
b[i] = sessionChars[r[i]%byte(len(sessionChars))]
}
return string(b)
}
......@@ -157,22 +157,38 @@ type SessionResult struct {
Missing [][]irmaclient.DisclosureCandidates
}
// UnsatisfiableTestHandler is a session handler that expects RequestVerificationPermission
// to be called for an unsatisfiable session. If called a second time, it checks
// that the session has beome satifsiable and finishes it.
type UnsatisfiableTestHandler struct {
TestHandler
called bool
}
func (th UnsatisfiableTestHandler) Success(result string) {
th.Failure(&irma.SessionError{ErrorType: irma.ErrorType("Unsatisfiable request succeeded")})
func (th *UnsatisfiableTestHandler) Success(result string) {
if !th.called {
th.Failure(&irma.SessionError{ErrorType: irma.ErrorType("Unsatisfiable request succeeded early")})
} else {
th.c <- nil
}
}
func (th UnsatisfiableTestHandler) RequestVerificationPermission(request *irma.DisclosureRequest, satisfiable bool, candidates [][]irmaclient.DisclosureCandidates, ServerName irma.TranslatedString, callback irmaclient.PermissionHandler) {
if satisfiable {
th.Failure(&irma.SessionError{ErrorType: irma.ErrorType("Unsatisfiable request succeeded")})
return
func (th *UnsatisfiableTestHandler) RequestVerificationPermission(request *irma.DisclosureRequest, satisfiable bool, candidates [][]irmaclient.DisclosureCandidates, ServerName irma.TranslatedString, callback irmaclient.PermissionHandler) {
if !th.called {
if satisfiable {
th.Failure(&irma.SessionError{ErrorType: irma.ErrorType("Unsatisfiable request succeeded")})
return
}
th.called = true
th.c <- &SessionResult{Missing: candidates}
} else {
th.TestHandler.RequestVerificationPermission(request, satisfiable, candidates, ServerName, callback)
}
th.c <- &SessionResult{Missing: candidates}
}
// Override TestHandler.Cancelled() so we can cancel future RequestVerificationPermission() invocations
func (th *UnsatisfiableTestHandler) Cancelled() {}
// ManualTestHandler embeds a TestHandler to inherit its methods.
// Below we overwrite the methods that require behaviour specific to manual settings.
type ManualTestHandler struct {
......
......@@ -26,11 +26,13 @@ const (
sessionOptionIgnoreError
sessionOptionReuseServer
sessionOptionClientWait
sessionOptionWait
)
type requestorSessionResult struct {
*server.SessionResult
Missing [][]irmaclient.DisclosureCandidates
Missing [][]irmaclient.DisclosureCandidates
Dismisser irmaclient.SessionDismisser
}
func processOptions(options ...sessionOption) sessionOption {
......@@ -54,7 +56,7 @@ func requestorSessionHelper(t *testing.T, request irma.SessionRequest, client *i
defer StopIrmaServer()
}
clientChan := make(chan *SessionResult)
clientChan := make(chan *SessionResult, 2)
serverChan := make(chan *server.SessionResult)
qr, token, err := irmaServer.StartSession(request, func(result *server.SessionResult) {
......@@ -64,7 +66,7 @@ func requestorSessionHelper(t *testing.T, request irma.SessionRequest, client *i
var h irmaclient.Handler
if opts&sessionOptionUnsatisfiableRequest > 0 {
h = &UnsatisfiableTestHandler{TestHandler{t, clientChan, client, nil, 0, ""}}
h = &UnsatisfiableTestHandler{TestHandler: TestHandler{t, clientChan, client, nil, 0, ""}}
} else {
var wait time.Duration = 0
if opts&sessionOptionClientWait > 0 {
......@@ -75,15 +77,15 @@ func requestorSessionHelper(t *testing.T, request irma.SessionRequest, client *i
j, err := json.Marshal(qr)
require.NoError(t, err)
client.NewSession(string(j), h)
dismisser := client.NewSession(string(j), h)
clientResult := <-clientChan
if opts&sessionOptionIgnoreError == 0 && clientResult != nil {
require.NoError(t, clientResult.Err)
}
if opts&sessionOptionUnsatisfiableRequest > 0 {
if opts&sessionOptionUnsatisfiableRequest > 0 && opts&sessionOptionWait == 0 {
require.NotNil(t, clientResult)
return &requestorSessionResult{nil, clientResult.Missing}
return &requestorSessionResult{nil, clientResult.Missing, dismisser}
}
serverResult := <-serverChan
......@@ -103,7 +105,7 @@ func requestorSessionHelper(t *testing.T, request irma.SessionRequest, client *i
require.NoError(t, err)
}
return &requestorSessionResult{serverResult, nil}
return &requestorSessionResult{serverResult, nil, dismisser}
}
// Check that nonexistent IRMA identifiers in the session request fail the session
......@@ -391,3 +393,45 @@ func TestClientDeveloperMode(t *testing.T) {
require.Equal(t, string(irma.ErrorHTTPS), string(serr.ErrorType))
require.Equal(t, "remote server does not use https", serr.Err.Error())
}
func TestParallelSessions(t *testing.T) {
client, handler := parseStorage(t)
defer test.ClearTestStorage(t, handler.storage)
StartIrmaServer(t, false, "")
defer StopIrmaServer()
// Ensure we don't have the requested attribute at first
require.NoError(t, client.RemoveStorage())
client.SetPreferences(irmaclient.Preferences{DeveloperMode: true})
// Start disclosure session for an attribute we don't have.
// sessionOptionWait makes this block until the IRMA server returns a result.
disclosure := make(chan *requestorSessionResult)
go func() {
disclosure <- requestorSessionHelper(t,
getDisclosureRequest(irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")),
client,
sessionOptionUnsatisfiableRequest, sessionOptionReuseServer, sessionOptionWait,
)
}()
// Wait for a bit then check that so far zero sessions have been done
time.Sleep(100 * time.Millisecond)
logs, err := client.LoadNewestLogs(100)
require.NoError(t, err)
require.Zero(t, len(logs))
// Issue credential containing above attribute
requestorSessionHelper(t, getIssuanceRequest(false), client, sessionOptionReuseServer)
// Running disclosure session should now finish using the new credential
result := <-disclosure
require.Nil(t, result.Err)
require.NotEmpty(t, result.Disclosed)
require.Equal(t, "s1234567", result.Disclosed[0][0].Value["en"])
// Two sessions should now have been done
logs, err = client.LoadNewestLogs(100)
require.NoError(t, err)
require.Len(t, logs, 2)
}
......@@ -82,6 +82,8 @@ func testRevocation(t *testing.T, attr irma.AttributeTypeIdentifier, client *irm
logger.Info("step 5")
result = revocationSession(t, client, request, sessionOptionUnsatisfiableRequest)
require.NotEmpty(t, result.Missing)
require.NotNil(t, result.Dismisser)
result.Dismisser.Dismiss()
// client revocation callback was called
require.NotNil(t, handler.(*TestClientHandler).revoked)
require.Equal(t, credid, handler.(*TestClientHandler).revoked.Type)
......
......@@ -60,6 +60,7 @@ type Client struct {
Configuration *irma.Configuration
irmaConfigurationPath string
handler ClientHandler
sessions sessions
jobs chan func() // queue of jobs to run
jobsPause chan struct{} // sending pauses background jobs
......@@ -213,6 +214,8 @@ func New(
}
}
client.sessions = sessions{client: client, sessions: map[string]*session{}}
client.jobs = make(chan func(), 100)
client.initRevocation()
client.StartJobs()
......
......@@ -14,6 +14,7 @@ import (
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/common"
)
// This file contains the logic and state of performing IRMA sessions, communicates
......@@ -72,6 +73,7 @@ type session struct {
Version *irma.ProtocolVersion
ServerName irma.TranslatedString
token string
choice *irma.DisclosureChoice
attrIndices irma.DisclosedAttributeIndices
client *Client
......@@ -92,6 +94,11 @@ type session struct {
transport *irma.HTTPTransport
}
type sessions struct {
client *Client
sessions map[string]*session
}
// We implement the handler for the keyshare protocol
var _ keyshareSessionHandler = (*session)(nil)
......@@ -156,6 +163,7 @@ func (client *Client) newManualSession(request irma.SessionRequest, handler Hand
request: request,
prepRevocation: make(chan error),
}
client.sessions.add(session)
session.Handler.StatusUpdate(session.Action, irma.StatusManualStarted)
session.processSessionInfo()
......@@ -190,6 +198,7 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) SessionDismisse
client: client,
prepRevocation: make(chan error),
}
client.sessions.add(session)
session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating)
min := minVersion
......@@ -333,6 +342,15 @@ func (session *session) processSessionInfo() {
irma.Logger.Debug("starting candidate computation before revocation witnesses updating finished")
}
// Handle ClientReturnURL if one is found in the session request
if session.request.Base().ClientReturnURL != "" {
session.Handler.ClientReturnURLSet(session.request.Base().ClientReturnURL)
}
session.requestPermission()
}
func (session *session) requestPermission() {
candidates, satisfiable, err := session.client.Candidates(session.request)
if err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err})
......@@ -341,11 +359,6 @@ func (session *session) processSessionInfo() {
session.Handler.StatusUpdate(session.Action, irma.StatusConnected)
// Handle ClientReturnURL if one is found in the session request
if session.request.Base().ClientReturnURL != "" {
session.Handler.ClientReturnURLSet(session.request.Base().ClientReturnURL)
}
// Ask for permission to execute the session
switch session.Action {
case irma.ActionDisclosing:
......@@ -393,6 +406,7 @@ func (session *session) doSession(proceed bool, choice *irma.DisclosureChoice) {
return
}
session.sendResponse(message)
session.finish(false)
} else {
var err error
session.builders, session.attrIndices, session.issuerProofNonce, err = session.getBuilders()
......@@ -497,7 +511,7 @@ func (session *session) sendResponse(message interface{}) {
if session.Action == irma.ActionIssuing {
session.client.handler.UpdateAttributes()
}
session.done = true
session.finish(false)
session.client.nonrevRepopulateCaches(session.request)
session.client.StartJobs()
session.Handler.Success(string(messageJson))
......@@ -548,6 +562,7 @@ func (session *session) checkKeyshareEnrollment() bool {
distributed := session.client.Configuration.SchemeManagers[id].Distributed()
_, enrolled := session.client.keyshareServers[id]
if distributed && !enrolled {
session.finish(false)
session.Handler.KeyshareEnrollmentMissing(id)
return false
}
......@@ -619,6 +634,7 @@ func (session *session) Distributed() bool {
func (session *session) recoverFromPanic() {
if e := recover(); e != nil {
session.finish(false)
if session.Handler != nil {
session.Handler.Failure(panicToError(e))
}
......@@ -643,13 +659,13 @@ func panicToError(e interface{}) *irma.SessionError {
// finish the session, by sending a DELETE to the server if there is one, and restarting local
// background jobs. This function is idempotent, doing nothing when called a second time. It
// returns whether or not it did something.
func (session *session) finish() bool {
func (session *session) finish(delete bool) bool {
if !session.done {
if session.IsInteractive() {
if delete && session.IsInteractive() {
session.transport.Delete()
}
session.client.nonrevRepopulateCaches(session.request)
session.client.StartJobs()
session.client.sessions.remove(session.token)
session.done = true
return true
}
......@@ -657,7 +673,7 @@ func (session *session) finish() bool {
}
func (session *session) fail(err *irma.SessionError) {
if session.finish() && err.ErrorType != irma.ErrorKeyshareUnenrolled {
if session.finish(true) && err.ErrorType != irma.ErrorKeyshareUnenrolled {
irma.Logger.Warn("client session error: ", err.Error())
err.Err = errors.Wrap(err.Err, 0)
session.Handler.Failure(err)
......@@ -665,7 +681,7 @@ func (session *session) fail(err *irma.SessionError) {
}
func (session *session) cancel() {
if session.finish() {
if session.finish(true) {
session.Handler.Cancelled()
}
}
......@@ -698,14 +714,17 @@ func (session *session) KeyshareCancelled() {
}
func (session *session) KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier) {
session.finish(false)
session.Handler.KeyshareEnrollmentIncomplete(manager)
}
func (session *session) KeyshareEnrollmentDeleted(manager irma.SchemeManagerIdentifier) {
session.finish(false)
session.Handler.KeyshareEnrollmentDeleted(manager)
}
func (session *session) KeyshareBlocked(manager irma.SchemeManagerIdentifier, duration int) {
session.finish(false)
session.Handler.KeyshareBlocked(manager, duration)
}
......@@ -727,3 +746,23 @@ func (session *session) KeysharePin() {
func (session *session) KeysharePinOK() {
session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating)
}
func (s sessions) remove(token string) {
last := s.sessions[token]
delete(s.sessions, token)
if last.Action == irma.ActionIssuing {
for _, session := range s.sessions {
session.requestPermission()
}
}
if len(s.sessions) == 0 {
s.client.StartJobs()
}
}
func (s sessions) add(session *session) {
session.token = common.NewSessionToken()
s.sessions[session.token] = session
}
package irmaserver
import (
"crypto/rand"
"sync"
"time"
......@@ -67,7 +66,6 @@ type memorySessionStore struct {
const (
maxSessionLifetime = 5 * time.Minute // After this a session is cancelled
sessionChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
var (
......@@ -153,8 +151,8 @@ func (s *memorySessionStore) deleteExpired() {
var one *big.Int = big.NewInt(1)
func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) *session {
token := newSessionToken()
clientToken := newSessionToken()
token := common.NewSessionToken()
clientToken := common.NewSessionToken()
ses := &session{
action: action,
......@@ -184,19 +182,3 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) *
return ses
}
func newSessionToken() string {
count := 20
r := make([]byte, count)
_, err := rand.Read(r)
if err != nil {
panic(err)
}
b := make([]byte, count)
for i := range b {
b[i] = sessionChars[r[i]%byte(len(sessionChars))]
}
return string(b)
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment