Commit 12e15c38 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Merge branch 'wizard'

parents 68675054 db988407
......@@ -43,7 +43,7 @@ func (attrs *AttributeList) CredentialInfo() *CredentialInfo {
}
func (ci CredentialInfo) GetCredentialType(conf *Configuration) *CredentialType {
return conf.CredentialTypes[NewCredentialTypeIdentifier(fmt.Sprintf("%s.%s.%s", ci.SchemeManagerID, ci.IssuerID, ci.ID))]
return conf.CredentialTypes[ci.Identifier()]
}
// Returns true if credential is expired at moment of calling this function
......@@ -51,6 +51,10 @@ func (ci CredentialInfo) IsExpired() bool {
return ci.Expires.Before(Timestamp(time.Now()))
}
func (ci CredentialInfo) Identifier() CredentialTypeIdentifier {
return NewCredentialTypeIdentifier(fmt.Sprintf("%s.%s.%s", ci.SchemeManagerID, ci.IssuerID, ci.ID))
}
// Len implements sort.Interface.
func (cl CredentialInfoList) Len() int {
return len(cl)
......
This diff is collapsed.
......@@ -25,6 +25,14 @@ type RequestorSchemeIdentifier struct {
metaObjectIdentifier
}
type RequestorIdentifier struct {
metaObjectIdentifier
}
type IssueWizardIdentifier struct {
metaObjectIdentifier
}
// SchemeManagerIdentifier identifies a scheme manager. Equal to its ID. For example "irma-demo".
type SchemeManagerIdentifier struct {
metaObjectIdentifier
......@@ -113,11 +121,21 @@ func (oi metaObjectIdentifier) Root() string {
}
}
// NewRequestorIdentifier converts the specified identifier to a RequestorSchemeIdentifier.
// NewRequestorSchemeIdentifier converts the specified identifier to a RequestorSchemeIdentifier.
func NewRequestorSchemeIdentifier(id string) RequestorSchemeIdentifier {
return RequestorSchemeIdentifier{metaObjectIdentifier(id)}
}
// NewRequestorIdentifier converts the specified identifier to a NewRequestorIdentifier.
func NewRequestorIdentifier(id string) RequestorIdentifier {
return RequestorIdentifier{metaObjectIdentifier(id)}
}
// NewIssueWizardIdentifier converts the specified identifier to a NewIssueWizardIdentifier.
func NewIssueWizardIdentifier(id string) IssueWizardIdentifier {
return IssueWizardIdentifier{metaObjectIdentifier(id)}
}
// NewSchemeManagerIdentifier converts the specified identifier to a SchemeManagerIdentifier.
func NewSchemeManagerIdentifier(id string) SchemeManagerIdentifier {
return SchemeManagerIdentifier{metaObjectIdentifier(id)}
......@@ -138,6 +156,16 @@ func NewAttributeTypeIdentifier(id string) AttributeTypeIdentifier {
return AttributeTypeIdentifier{metaObjectIdentifier(id)}
}
// RequestorIdentifier returns the requestor identifier of the issue wizard.
func (id IssueWizardIdentifier) RequestorIdentifier() RequestorIdentifier {
return NewRequestorIdentifier(id.Parent())
}
// RequestorSchemeIdentifier returns the requestor scheme identifier of the requestor.
func (id RequestorIdentifier) RequestorSchemeIdentifier() RequestorSchemeIdentifier {
return NewRequestorSchemeIdentifier(id.Parent())
}
// SchemeManagerIdentifier returns the scheme manager identifer of the issuer.
func (id IssuerIdentifier) SchemeManagerIdentifier() SchemeManagerIdentifier {
return NewSchemeManagerIdentifier(id.Parent())
......@@ -148,6 +176,10 @@ func (id CredentialTypeIdentifier) IssuerIdentifier() IssuerIdentifier {
return NewIssuerIdentifier(id.Parent())
}
func (id CredentialTypeIdentifier) SchemeManagerIdentifier() SchemeManagerIdentifier {
return NewSchemeManagerIdentifier(id.Root())
}
// CredentialTypeIdentifier returns the CredentialTypeIdentifier of the attribute identifier.
func (id AttributeTypeIdentifier) CredentialTypeIdentifier() CredentialTypeIdentifier {
if id.IsCredential() {
......@@ -178,15 +210,39 @@ func (id *SchemeManagerIdentifier) UnmarshalText(text []byte) error {
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (id RequestorSchemeIdentifier) MarshalText() ([]byte, error) {
return []byte(id.String()), nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (id *RequestorSchemeIdentifier) UnmarshalText(text []byte) error {
*id = NewRequestorSchemeIdentifier(string(text))
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (id RequestorIdentifier) MarshalText() ([]byte, error) {
return []byte(id.String()), nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (id *RequestorIdentifier) UnmarshalText(text []byte) error {
*id = NewRequestorIdentifier(string(text))
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (id IssueWizardIdentifier) MarshalText() ([]byte, error) {
return []byte(id.String()), nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (id *IssueWizardIdentifier) UnmarshalText(text []byte) error {
*id = NewIssueWizardIdentifier(string(text))
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (id IssuerIdentifier) MarshalText() ([]byte, error) {
return []byte(id.String()), nil
......
......@@ -44,7 +44,7 @@ func processOptions(options ...sessionOption) sessionOption {
return opts
}
func requestorSessionHelper(t *testing.T, request irma.SessionRequest, client *irmaclient.Client, options ...sessionOption) *requestorSessionResult {
func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclient.Client, options ...sessionOption) *requestorSessionResult {
if client == nil {
var handler *TestClientHandler
client, handler = parseStorage(t)
......
package sessiontest
import (
"encoding/json"
"io/ioutil"
"net/http"
"path/filepath"
"testing"
......@@ -19,6 +21,7 @@ import (
var (
httpServer *http.Server
nextRequestServer *http.Server
irmaServer *irmaserver.Server
irmaServerConfiguration *server.Configuration
requestorServer *requestorserver.Server
......@@ -104,6 +107,95 @@ func StopIrmaServer() {
_ = httpServer.Close()
}
func chainedServerHandler(t *testing.T) http.Handler {
mux := http.NewServeMux()
id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")
// Note: these chained session requests just serve to test the full functionality of this
// feature, and don't necessarily represent a chain of sessions that would be sensible or
// desirable in production settings; probably a chain should not be longer than two sessions,
// with an issuance session at the end.
mux.HandleFunc("/1", func(w http.ResponseWriter, r *http.Request) {
request := &irma.ServiceProviderRequest{
Request: getDisclosureRequest(id),
RequestorBaseRequest: irma.RequestorBaseRequest{
NextSession: &irma.NextSessionData{URL: "http://localhost:48686/2"},
},
}
bts, err := json.Marshal(request)
require.NoError(t, err)
_, err = w.Write(bts)
require.NoError(t, err)
})
var attr *string
mux.HandleFunc("/2", func(w http.ResponseWriter, r *http.Request) {
bts, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
require.NoError(t, r.Body.Close())
var result server.SessionResult
require.NoError(t, json.Unmarshal(bts, &result))
require.Equal(t, irma.ProofStatusValid, result.ProofStatus)
require.Len(t, result.Disclosed, 1)
require.Len(t, result.Disclosed[0], 1)
attr = result.Disclosed[0][0].RawValue
require.NotNil(t, attr)
cred := &irma.CredentialRequest{
CredentialTypeID: id.CredentialTypeIdentifier(),
Attributes: map[string]string{
"level": *attr,
"studentCardNumber": *attr,
"studentID": *attr,
"university": *attr,
},
}
bts, err = json.Marshal(irma.IdentityProviderRequest{
Request: irma.NewIssuanceRequest([]*irma.CredentialRequest{cred}),
RequestorBaseRequest: irma.RequestorBaseRequest{
NextSession: &irma.NextSessionData{URL: "http://localhost:48686/3"},
},
})
require.NoError(t, err)
logger.Trace("2nd request: ", string(bts))
_, err = w.Write(bts)
require.NoError(t, err)
})
mux.HandleFunc("/3", func(w http.ResponseWriter, r *http.Request) {
request := irma.NewDisclosureRequest()
request.Disclose = irma.AttributeConDisCon{{{{
Type: irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.level"),
Value: attr,
}}}}
bts, err := json.Marshal(request)
require.NoError(t, err)
logger.Trace("3rd request: ", string(bts))
_, err = w.Write(bts)
require.NoError(t, err)
})
return mux
}
func StartNextRequestServer(t *testing.T) {
nextRequestServer = &http.Server{
Addr: "localhost:48686",
Handler: chainedServerHandler(t),
}
go func() {
_ = nextRequestServer.ListenAndServe()
}()
}
func StopNextRequestServer() {
_ = nextRequestServer.Close()
}
var IrmaServerConfiguration = &requestorserver.Configuration{
Configuration: &server.Configuration{
URL: "http://localhost:48682/irma",
......
......@@ -4,7 +4,8 @@ import (
"bytes"
"context"
"encoding/json"
"github.com/dgrijalva/jwt-go"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
......@@ -13,10 +14,12 @@ import (
"time"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/irmago"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test"
"github.com/privacybydesign/irmago/irmaclient"
"github.com/privacybydesign/irmago/server"
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/require"
)
......@@ -603,3 +606,26 @@ func TestPOSTSizeLimit(t *testing.T) {
require.NoError(t, json.Unmarshal(bts, &rerr))
require.Equal(t, "http: request body too large", rerr.Message)
}
func TestChainedSessions(t *testing.T) {
client, handler := parseStorage(t)
defer test.ClearTestStorage(t, handler.storage)
StartNextRequestServer(t)
defer StopNextRequestServer()
var request irma.ServiceProviderRequest
require.NoError(t, irma.NewHTTPTransport("http://localhost:48686", false).Get("1", &request))
requestorSessionHelper(t, &request, client)
// check that our credential instance is new
id := request.SessionRequest().Disclosure().Disclose[0][0][0].Type.CredentialTypeIdentifier()
for _, cred := range client.CredentialInfoList() {
if id.String() == fmt.Sprintf("%s.%s.%s", cred.SchemeManagerID, cred.IssuerID, cred.ID) &&
cred.SignedOn.After(irma.Timestamp(time.Now().Add(-1*irma.ExpiryFactor*time.Second))) {
return
}
}
require.NoError(t, errors.New("newly issued credential not found in client"))
}
......@@ -80,6 +80,9 @@ type session struct {
done <-chan struct{}
prepRevocation chan error // used when nonrevocation preprocessing is done
next *session
implicitDisclosure [][]*irma.AttributeIdentifier
// State for issuance sessions
issuerProofNonce *big.Int
builders gabi.ProofBuilderList
......@@ -107,6 +110,7 @@ var supportedVersions = map[int][]int{
4, // old protocol with legacy session requests
5, // introduces condiscon feature
6, // introduces nonrevocation proofs
7, // introduces chained sessions
},
}
var minVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][0]}
......@@ -174,7 +178,7 @@ func (client *Client) newManualSession(request irma.SessionRequest, handler Hand
}
// newQrSession creates and starts a new interactive IRMA session
func (client *Client) newQrSession(qr *irma.Qr, handler Handler) SessionDismisser {
func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session {
if qr.Type == irma.ActionRedirect {
newqr := &irma.Qr{}
transport := irma.NewHTTPTransport("", !client.Preferences.DeveloperMode)
......@@ -408,6 +412,12 @@ func (session *session) doSession(proceed bool, choice *irma.DisclosureChoice) {
session.cancel()
return
}
// If this is a session in a chain of sessions, also disclose all attributes disclosed in previous sessions
if session.implicitDisclosure != nil {
choice.Attributes = append(choice.Attributes, session.implicitDisclosure...)
}
session.choice = choice
if err := session.choice.Validate(); err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorRequiredAttributeMissing, Err: err})
......@@ -458,6 +468,9 @@ func (session *session) sendResponse(message interface{}) {
var log *LogEntry
var err error
var messageJson []byte
var path string
var ourResponse interface{}
serverResponse := &irma.ServerSessionResponse{ProtocolVersion: session.Version, SessionType: session.Action}
switch session.Action {
case irma.ActionSigning:
......@@ -466,68 +479,48 @@ func (session *session) sendResponse(message interface{}) {
session.fail(&irma.SessionError{ErrorType: irma.ErrorSerialization, Info: "Type assertion failed"})
return
}
messageJson, err = json.Marshal(irmaSignature)
if err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorSerialization, Err: err})
return
}
if session.IsInteractive() {
var response disclosureResponse
if err = session.transport.Post("proofs", &response, irmaSignature); err != nil {
session.fail(err.(*irma.SessionError))
return
}
if response != "VALID" {
session.fail(&irma.SessionError{ErrorType: irma.ErrorRejected, Info: string(response)})
return
}
}
log, err = session.createLogEntry(message)
if err != nil {
irma.Logger.Warn(errors.WrapPrefix(err, "Failed to create log entry", 0).ErrorStack())
session.client.reportError(err)
}
ourResponse = irmaSignature
path = "proofs"
case irma.ActionDisclosing:
messageJson, err = json.Marshal(message)
if err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorSerialization, Err: err})
return
}
if session.IsInteractive() {
var response disclosureResponse
if err = session.transport.Post("proofs", &response, message); err != nil {
session.fail(err.(*irma.SessionError))
return
}
if response != "VALID" {
session.fail(&irma.SessionError{ErrorType: irma.ErrorRejected, Info: string(response)})
return
}
}
log, err = session.createLogEntry(message)
if err != nil {
irma.Logger.Warn(errors.WrapPrefix(err, "Failed to create log entry", 0).ErrorStack())
session.client.reportError(err)
}
ourResponse = message
path = "proofs"
case irma.ActionIssuing:
response := []*gabi.IssueSignatureMessage{}
if err = session.transport.Post("commitments", &response, message); err != nil {
ourResponse = message
path = "commitments"
}
if session.IsInteractive() {
if err = session.transport.Post(path, &serverResponse, ourResponse); err != nil {
session.fail(err.(*irma.SessionError))
return
}
if err = session.client.ConstructCredentials(response, session.request.(*irma.IssuanceRequest), session.builders); err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err})
if serverResponse.ProofStatus != irma.ProofStatusValid {
session.fail(&irma.SessionError{ErrorType: irma.ErrorRejected, Info: string(serverResponse.ProofStatus)})
return
}
log, err = session.createLogEntry(message)
if err != nil {
irma.Logger.Warn(errors.WrapPrefix(err, "Failed to create log entry", 0).ErrorStack())
session.client.reportError(err)
if session.Action == irma.ActionIssuing {
if err = session.client.ConstructCredentials(serverResponse.IssueSignatures, session.request.(*irma.IssuanceRequest), session.builders); err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err})
return
}
}
}
log, err = session.createLogEntry(message)
if err != nil {
irma.Logger.Warn(errors.WrapPrefix(err, "Failed to create log entry", 0).ErrorStack())
session.client.reportError(err)
}
if err = session.client.storage.AddLogEntry(log); err != nil {
irma.Logger.Warn(errors.WrapPrefix(err, "Failed to write log entry", 0).ErrorStack())
}
......@@ -535,7 +528,13 @@ func (session *session) sendResponse(message interface{}) {
session.client.handler.UpdateAttributes()
}
session.finish(false)
session.Handler.Success(string(messageJson))
if serverResponse != nil && serverResponse.NextSession != nil {
session.next = session.client.newQrSession(serverResponse.NextSession, session.Handler)
session.next.implicitDisclosure = session.choice.Attributes
} else {
session.Handler.Success(string(messageJson))
}
}
// Response calculation methods
......@@ -716,7 +715,11 @@ func (session *session) cancel() {
}
func (session *session) Dismiss() {
session.cancel()
if session.next != nil {
session.next.Dismiss()
} else {
session.cancel()
}
}
// Keyshare session handler methods
......
......@@ -41,6 +41,8 @@ type Configuration struct {
RequestorSchemes map[RequestorSchemeIdentifier]*RequestorScheme
Requestors map[string]*RequestorInfo
IssueWizards map[IssueWizardIdentifier]*IssueWizard
// DisabledRequestorSchemes keeps track of any error of the requestorscheme if it
// did not parse successfully
DisabledRequestorSchemes map[RequestorSchemeIdentifier]*SchemeManagerError
......@@ -132,23 +134,42 @@ func (conf *Configuration) ParseFolder() (err error) {
}
}
// Parse schemes in storage
// Since requestor schemes may contain information defined in issuer schemes, first check
// what schemes exist so we can parse issuer schemes first.
var mgrerr *SchemeManagerError
var issuerschemes, requestorschemes []Scheme
err = common.IterateSubfolders(conf.Path, func(dir string, _ os.FileInfo) error {
_, err := conf.ParseSchemeFolder(dir)
scheme, _, err := conf.parseSchemeDescription(dir)
if err != nil {
return err
}
switch scheme.typ() {
case SchemeTypeIssuer:
issuerschemes = append(issuerschemes, scheme)
case SchemeTypeRequestor:
requestorschemes = append(requestorschemes, scheme)
default:
return errors.New("unsupported scheme type")
}
return nil
})
if err != nil {
return
}
// Parse the schemes we found, issuer schemes first
for _, scheme := range append(issuerschemes, requestorschemes...) {
_, err := conf.ParseSchemeFolder(scheme.path())
if err == nil {
return nil // OK, do next scheme folder
continue // OK, do next scheme folder
}
// If there is an error, and it is of type SchemeManagerError, return nil
// so as to continue parsing other schemes.
if e, ok := err.(*SchemeManagerError); ok {
mgrerr = e
return nil
continue
}
return err // Not a SchemeManagerError? return it & halt parsing now
})
if err != nil {
return
}
if !conf.options.IgnorePrivateKeys && len(conf.PrivateKeys.(*privateKeyRingMerge).rings) == 0 {
......@@ -500,6 +521,7 @@ func (conf *Configuration) clear() {
conf.DisabledSchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManagerError)
conf.RequestorSchemes = make(map[RequestorSchemeIdentifier]*RequestorScheme)
conf.Requestors = make(map[string]*RequestorInfo)
conf.IssueWizards = make(map[IssueWizardIdentifier]*IssueWizard)
conf.DisabledRequestorSchemes = make(map[RequestorSchemeIdentifier]*SchemeManagerError)
conf.kssPublicKeys = make(map[SchemeManagerIdentifier]map[int]*rsa.PublicKey)
conf.publicKeys = make(map[IssuerIdentifier]map[uint]*gabi.PublicKey)
......@@ -723,7 +745,8 @@ func (conf *Configuration) validateTranslations(file string, o interface{}) {
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
name := v.Type().Field(i).Name
if field.Type() != reflect.TypeOf(TranslatedString{}) ||
translatedString := TranslatedString{}
if (field.Type() != reflect.TypeOf(translatedString) && field.Type() != reflect.TypeOf(&translatedString)) ||
name == "IssueURL" ||
name == "Category" ||
name == "FAQIntro" ||
......@@ -732,10 +755,21 @@ func (conf *Configuration) validateTranslations(file string, o interface{}) {
name == "FAQHowto" {
continue
}
val := field.Interface().(TranslatedString)
for _, lang := range validLangs {
if _, exists := val[lang]; !exists {
conf.Warnings = append(conf.Warnings, fmt.Sprintf("%s misses %s translation in <%s> tag", file, lang, name))
var val TranslatedString
if field.Type() == reflect.TypeOf(&translatedString) {
tmp := field.Interface().(*TranslatedString)
if tmp == nil {
return
}
val = *tmp
} else {
val = field.Interface().(TranslatedString)
}
// assuming that translations also never should be empty
if l := val.validate(); len(l) > 0 {
for _, invalidLang := range l {
conf.Warnings = append(conf.Warnings, fmt.Sprintf("%s misses %s translation in <%s> tag", file, invalidLang, name))
}
}
}
......@@ -769,6 +803,9 @@ func (conf *Configuration) join(other *Configuration) {
for key, val := range other.Requestors {
conf.Requestors[key] = val
}
for key, val := range other.IssueWizards {
conf.IssueWizards[key] = val
}
for key, val := range other.DisabledRequestorSchemes {
conf.DisabledRequestorSchemes[key] = val
}
......
This diff is collapsed.
......@@ -318,3 +318,36 @@ func (ir *IssuanceRequest) UnmarshalJSON(bts []byte) (err error) {
return checkType(legacy.Type, ActionIssuing)
}
func (s *ServerSessionResponse) MarshalJSON() ([]byte, error) {
if !s.ProtocolVersion.Below(2, 7) {
type response ServerSessionResponse
return json.Marshal((*response)(s))
}
if s.NextSession != nil {
return nil, errors.New("cannot marshal next session pointer into legacy server session response")
}
if s.SessionType != ActionIssuing {
return json.Marshal(s.ProofStatus)
}
return json.Marshal(s.IssueSignatures)
}
func (s *ServerSessionResponse) UnmarshalJSON(bts []byte) error {
if !s.ProtocolVersion.Below(2, 7) {
type response ServerSessionResponse
return json.Unmarshal(bts, (*response)(s))
}
if s.SessionType != ActionIssuing {
return json.Unmarshal(bts, &s.ProofStatus)
}
err := json.Unmarshal(bts, &s.IssueSignatures)