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

fix: make irma server compatible with GET/POST retries from irmaclient

parent 5999bcfb
......@@ -353,6 +353,10 @@ func (s *Server) handleProtocolMessage(
return
}
if method == http.MethodGet {
status, output = session.checkCache(message, server.StatusConnected)
if len(output) != 0 {
return
}
h := http.Header(headers)
min := &irma.ProtocolVersion{}
max := &irma.ProtocolVersion{}
......@@ -365,10 +369,12 @@ func (s *Server) handleProtocolMessage(
return
}
status, output = server.JsonResponse(session.handleGetRequest(min, max))
session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusConnected}
return
}
status, output = server.JsonResponse(nil, session.fail(server.ErrorInvalidRequest, ""))
return
default:
if noun == "statusevents" {
err := server.RemoteError(server.ErrorInvalidRequest, "server sent events not supported by this server")
......@@ -388,30 +394,47 @@ func (s *Server) handleProtocolMessage(
}
if noun == "commitments" && session.action == irma.ActionIssuing {
status, output = session.checkCache(message, server.StatusDone)
if len(output) != 0 {
return
}
commitments := &irma.IssueCommitmentMessage{}
if err := irma.UnmarshalValidate(message, commitments); err != nil {
status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, ""))
if err = irma.UnmarshalValidate(message, commitments); err != nil {
status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
return
}
status, output = server.JsonResponse(session.handlePostCommitments(commitments))
session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
return
}
if noun == "proofs" && session.action == irma.ActionDisclosing {
disclosure := irma.Disclosure{}
if err := irma.UnmarshalValidate(message, &disclosure); err != nil {
status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, ""))
status, output = session.checkCache(message, server.StatusDone)
if len(output) != 0 {
return
}
disclosure := &irma.Disclosure{}
if err = irma.UnmarshalValidate(message, disclosure); err != nil {
status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
return
}
status, output = server.JsonResponse(session.handlePostDisclosure(disclosure))
session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
return
}
if noun == "proofs" && session.action == irma.ActionSigning {
status, output = session.checkCache(message, server.StatusDone)
if len(output) != 0 {
return
}
signature := &irma.SignedMessage{}
if err := irma.UnmarshalValidate(message, signature); err != nil {
status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, ""))
if err = irma.UnmarshalValidate(message, signature); err != nil {
status, output = server.JsonResponse(nil, session.fail(server.ErrorMalformedInput, err.Error()))
return
}
status, output = server.JsonResponse(session.handlePostSignature(signature))
session.responseCache = responseCache{message: message, response: output, status: status, sessionStatus: server.StatusDone}
return
}
......
......@@ -82,7 +82,7 @@ func (session *session) handlePostSignature(signature *irma.SignedMessage) (*irm
return &session.result.ProofStatus, rerr
}
func (session *session) handlePostDisclosure(disclosure irma.Disclosure) (*irma.ProofStatus, *irma.RemoteError) {
func (session *session) handlePostDisclosure(disclosure *irma.Disclosure) (*irma.ProofStatus, *irma.RemoteError) {
if session.status != server.StatusConnected {
return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished")
}
......
package servercore
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
......@@ -47,6 +48,30 @@ func (session *session) fail(err server.Error, message string) *irma.RemoteError
return rerr
}
const retryTimeLimit = 5 * time.Second
// checkCache returns a previously cached response, for replaying against multiple requests from
// irmago's retryablehttp client, if:
// - the same was POSTed as last time
// - last time was not more than 5 seconds ago (retryablehttp client gives up before this)
// - the status is now done (which it should be if this is the second time we receive this message).
func (session *session) checkCache(message []byte, expectedStatus server.Status) (int, []byte) {
if len(session.responseCache.response) > 0 {
if session.responseCache.sessionStatus != expectedStatus {
// don't replay a cache value that was set in a previous session state
session.responseCache = responseCache{}
return 0, nil
}
if sha256.Sum256(session.responseCache.message) != sha256.Sum256(message) ||
session.lastActive.Before(time.Now().Add(-retryTimeLimit)) ||
session.status != expectedStatus {
return server.JsonResponse(nil, session.fail(server.ErrorUnexpectedRequest, ""))
}
return session.responseCache.status, session.responseCache.response
}
return 0, nil
}
// Issuance helpers
func (s *Server) validateIssuanceRequest(request *irma.IssuanceRequest) error {
......
......@@ -24,9 +24,10 @@ type session struct {
request irma.SessionRequest
legacyCompatible bool // if the request is convertible to pre-condiscon format
status server.Status
prevStatus server.Status
evtSource eventsource.EventSource
status server.Status
prevStatus server.Status
evtSource eventsource.EventSource
responseCache responseCache
lastActive time.Time
result *server.SessionResult
......@@ -37,6 +38,13 @@ type session struct {
sessions sessionStore
}
type responseCache struct {
message []byte
response []byte
status int
sessionStatus server.Status
}
type sessionStore interface {
get(token string) *session
clientGet(token string) *session
......
......@@ -67,6 +67,7 @@ type TestHandler struct {
c chan *SessionResult
client *irmaclient.Client
expectedServerName irma.TranslatedString
result string
}
func (th TestHandler) KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier) {
......@@ -82,7 +83,8 @@ func (th TestHandler) KeyshareEnrollmentDeleted(manager irma.SchemeManagerIdenti
th.Failure(&irma.SessionError{Err: errors.Errorf("Keyshare enrollment deleted for %s", manager.String())})
}
func (th TestHandler) StatusUpdate(action irma.Action, status irma.Status) {}
func (th TestHandler) Success(result string) {
func (th *TestHandler) Success(result string) {
th.result = result
th.c <- nil
}
func (th TestHandler) Cancelled() {
......
......@@ -210,7 +210,7 @@ func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string
qr := startSession(t, request, sessiontype)
c := make(chan *SessionResult)
h := TestHandler{t, c, client, expectedServerName(t, request, client.Configuration)}
h := &TestHandler{t: t, c: c, client: client, expectedServerName: expectedServerName(t, request, client.Configuration)}
qrjson, err := json.Marshal(qr)
require.NoError(t, err)
client.NewSession(string(qrjson), h)
......
package sessiontest
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"reflect"
"testing"
"github.com/privacybydesign/irmago"
......@@ -15,8 +19,9 @@ import (
type sessionOption int
const (
sessionOptionUpdatedIrmaConfiguration = iota
sessionOptionUpdatedIrmaConfiguration sessionOption = 1 << iota
sessionOptionUnsatisfiableRequest
sessionOptionRetryPost
)
type requestorSessionResult struct {
......@@ -41,12 +46,18 @@ func requestorSessionHelper(t *testing.T, request irma.SessionRequest, client *i
})
require.NoError(t, err)
opts := 0
for _, o := range options {
opts |= int(o)
}
var h irmaclient.Handler
if len(options) == 1 && options[0] == sessionOptionUnsatisfiableRequest {
h = UnsatisfiableTestHandler{TestHandler{t, clientChan, client, nil}}
if opts&int(sessionOptionUnsatisfiableRequest) > 0 {
h = &UnsatisfiableTestHandler{TestHandler{t, clientChan, client, nil, ""}}
} else {
h = TestHandler{t, clientChan, client, nil}
h = &TestHandler{t, clientChan, client, nil, ""}
}
j, err := json.Marshal(qr)
require.NoError(t, err)
client.NewSession(string(j), h)
......@@ -55,13 +66,29 @@ func requestorSessionHelper(t *testing.T, request irma.SessionRequest, client *i
require.NoError(t, clientResult.Err)
}
if len(options) == 1 && options[0] == sessionOptionUnsatisfiableRequest {
if opts&int(sessionOptionUnsatisfiableRequest) > 0 {
require.NotNil(t, clientResult)
return &requestorSessionResult{nil, clientResult.Missing}
} else {
serverResult := <-serverChan
require.Equal(t, token, serverResult.Token)
return &requestorSessionResult{serverResult, nil}
}
serverResult := <-serverChan
require.Equal(t, token, serverResult.Token)
if opts&int(sessionOptionRetryPost) > 0 {
req, err := http.NewRequest(http.MethodPost,
qr.URL+"/proofs",
bytes.NewBuffer([]byte(h.(*TestHandler).result)),
)
require.NoError(t, err)
req.Header.Add("Content-Type", "application/json")
res, err := new(http.Client).Do(req)
require.NoError(t, err)
require.True(t, res.StatusCode < 300)
_, err = ioutil.ReadAll(res.Body)
require.NoError(t, err)
}
return &requestorSessionResult{serverResult, nil}
}
// Check that nonexistent IRMA identifiers in the session request fail the session
......@@ -75,16 +102,37 @@ func TestRequestorInvalidRequest(t *testing.T) {
require.Error(t, err)
}
func TestRequestorDoubleGET(t *testing.T) {
StartIrmaServer(t, false)
defer StopIrmaServer()
qr, _, err := irmaServer.StartSession(irma.NewDisclosureRequest(
irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID"),
), nil)
require.NoError(t, err)
// Simulate the first GET by the client in the session protocol, twice
var o interface{}
transport := irma.NewHTTPTransport(qr.URL)
transport.SetHeader(irma.MinVersionHeader, "2.5")
transport.SetHeader(irma.MaxVersionHeader, "2.5")
require.NoError(t, transport.Get("", &o))
require.NoError(t, transport.Get("", &o))
}
func TestRequestorSignatureSession(t *testing.T) {
client, _ := parseStorage(t)
id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
serverResult := requestorSessionHelper(t, irma.NewSignatureRequest("message", id), client)
require.Nil(t, serverResult.Err)
require.Equal(t, irma.ProofStatusValid, serverResult.ProofStatus)
require.NotEmpty(t, serverResult.Disclosed)
require.Equal(t, id, serverResult.Disclosed[0][0].Identifier)
require.Equal(t, "456", serverResult.Disclosed[0][0].Value["en"])
var serverResult *requestorSessionResult
for _, opt := range []sessionOption{0, sessionOptionRetryPost} {
serverResult = requestorSessionHelper(t, irma.NewSignatureRequest("message", id), client, opt)
require.Nil(t, serverResult.Err)
require.Equal(t, irma.ProofStatusValid, serverResult.ProofStatus)
require.NotEmpty(t, serverResult.Disclosed)
require.Equal(t, id, serverResult.Disclosed[0][0].Identifier)
require.Equal(t, "456", serverResult.Disclosed[0][0].Value["en"])
}
// Load the updated scheme in which an attribute was added to the studentCard credential type
schemeid := irma.NewSchemeManagerIdentifier("irma-demo")
......@@ -102,10 +150,12 @@ func TestRequestorSignatureSession(t *testing.T) {
func TestRequestorDisclosureSession(t *testing.T) {
id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
request := irma.NewDisclosureRequest(id)
serverResult := testRequestorDisclosure(t, request)
require.Len(t, serverResult.Disclosed, 1)
require.Equal(t, id, serverResult.Disclosed[0][0].Identifier)
require.Equal(t, "456", serverResult.Disclosed[0][0].Value["en"])
for _, opt := range []sessionOption{0, sessionOptionRetryPost} {
serverResult := testRequestorDisclosure(t, request, opt)
require.Len(t, serverResult.Disclosed, 1)
require.Equal(t, id, serverResult.Disclosed[0][0].Identifier)
require.Equal(t, "456", serverResult.Disclosed[0][0].Value["en"])
}
}
func TestRequestorDisclosureMultipleAttrs(t *testing.T) {
......@@ -117,8 +167,8 @@ func TestRequestorDisclosureMultipleAttrs(t *testing.T) {
require.Len(t, serverResult.Disclosed, 2)
}
func testRequestorDisclosure(t *testing.T, request *irma.DisclosureRequest) *server.SessionResult {
serverResult := requestorSessionHelper(t, request, nil)
func testRequestorDisclosure(t *testing.T, request *irma.DisclosureRequest, options ...sessionOption) *server.SessionResult {
serverResult := requestorSessionHelper(t, request, nil, options...)
require.Nil(t, serverResult.Err)
require.Equal(t, irma.ProofStatusValid, serverResult.ProofStatus)
return serverResult.SessionResult
......
......@@ -366,7 +366,7 @@ func TestDownloadSchemeManager(t *testing.T) {
URL: "http://localhost:48681/irma_configuration/irma-demo",
})
require.NoError(t, err)
client.NewSession(string(qr), TestHandler{t, c, client, nil})
client.NewSession(string(qr), &TestHandler{t: t, c: c, client: client, expectedServerName: nil})
if result := <-c; result != nil {
require.NoError(t, result.Err)
}
......@@ -419,7 +419,7 @@ func TestStaticQRSession(t *testing.T) {
c := make(chan *SessionResult)
// Perform session
client.NewSession(string(bts), TestHandler{t, c, client, host})
client.NewSession(string(bts), &TestHandler{t, c, client, host, ""})
if result := <-c; result != nil {
require.NoError(t, result.Err)
}
......
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