Commit 06821d1a authored by Tomas's avatar Tomas Committed by Sietse Ringers
Browse files

Changes for logs:


- Extract raw information from session, and only derive for display
- Add protocol version and ABS timestamps in log entries. Include an update function for existing logs
- AttributeValue of AttributeResult is now a TranslatedString
- Removed polymorphic 'response' from log entry; populate either IssueCommitment or Prooflist instead
- Greatly simplified marshalling log entries by not using a separate struct
- Added tests for disclosure logging
Co-authored-by: default avatarSietse Ringers <mail@sietseringers.net>
parent 5fae336e
...@@ -21,7 +21,7 @@ const ( ...@@ -21,7 +21,7 @@ const (
) )
type AttributeResult struct { type AttributeResult struct {
AttributeValue string `json:"value"` // Value of the disclosed attribute AttributeValue TranslatedString `json:"value"` // Value of the disclosed attribute
AttributeId AttributeTypeIdentifier `json:"id"` AttributeId AttributeTypeIdentifier `json:"id"`
AttributeProofStatus AttributeProofStatus `json:"status"` AttributeProofStatus AttributeProofStatus `json:"status"`
} }
...@@ -105,12 +105,20 @@ func (al *AttributeList) Strings() []TranslatedString { ...@@ -105,12 +105,20 @@ func (al *AttributeList) Strings() []TranslatedString {
if val == nil { if val == nil {
continue continue
} }
al.strings[i] = map[string]string{"en": *val, "nl": *val} // TODO al.strings[i] = translateAttribute(val)
} }
} }
return al.strings return al.strings
} }
// Localize raw attribute values (to be implemented)
func translateAttribute(attr *string) TranslatedString {
if attr == nil {
return nil
}
return map[string]string{"en": *attr, "nl": *attr}
}
func (al *AttributeList) decode(i int) *string { func (al *AttributeList) decode(i int) *string {
attr := al.Ints[i+1] attr := al.Ints[i+1]
metadataVersion := al.MetadataAttribute.Version() metadataVersion := al.MetadataAttribute.Version()
...@@ -340,7 +348,7 @@ type AttributeDisjunction struct { ...@@ -340,7 +348,7 @@ type AttributeDisjunction struct {
type DisclosedAttributeDisjunction struct { type DisclosedAttributeDisjunction struct {
AttributeDisjunction AttributeDisjunction
DisclosedValue string DisclosedValue TranslatedString
DisclosedId AttributeTypeIdentifier DisclosedId AttributeTypeIdentifier
ProofStatus AttributeProofStatus ProofStatus AttributeProofStatus
} }
...@@ -464,7 +472,7 @@ func (disclosedAttributeDisjunction *DisclosedAttributeDisjunction) MarshalJSON( ...@@ -464,7 +472,7 @@ func (disclosedAttributeDisjunction *DisclosedAttributeDisjunction) MarshalJSON(
Label string `json:"label"` Label string `json:"label"`
Attributes []AttributeTypeIdentifier `json:"attributes"` Attributes []AttributeTypeIdentifier `json:"attributes"`
DisclosedValue string `json:"disclosedValue"` DisclosedValue TranslatedString `json:"disclosedValue"`
DisclosedId AttributeTypeIdentifier `json:"disclosedId"` DisclosedId AttributeTypeIdentifier `json:"disclosedId"`
ProofStatus AttributeProofStatus `json:"proofStatus"` ProofStatus AttributeProofStatus `json:"proofStatus"`
}{ }{
......
...@@ -144,12 +144,11 @@ func (id IssuerIdentifier) MarshalText() ([]byte, error) { ...@@ -144,12 +144,11 @@ func (id IssuerIdentifier) MarshalText() ([]byte, error) {
return []byte(id.String()), nil return []byte(id.String()), nil
} }
// TODO enable this when updating protocol // UnmarshalText implements encoding.TextUnmarshaler.
//// UnmarshalText implements encoding.TextUnmarshaler. func (id *IssuerIdentifier) UnmarshalText(text []byte) error {
//func (id *IssuerIdentifier) UnmarshalText(text []byte) error { *id = NewIssuerIdentifier(string(text))
// *id = NewIssuerIdentifier(string(text)) return nil
// return nil }
//}
// MarshalText implements encoding.TextMarshaler. // MarshalText implements encoding.TextMarshaler.
func (id CredentialTypeIdentifier) MarshalText() ([]byte, error) { func (id CredentialTypeIdentifier) MarshalText() ([]byte, error) {
......
...@@ -630,7 +630,7 @@ func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, re ...@@ -630,7 +630,7 @@ func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, re
// we save none of them to fail the session cleanly // we save none of them to fail the session cleanly
gabicreds := []*gabi.Credential{} gabicreds := []*gabi.Credential{}
for i, sig := range msg { for i, sig := range msg {
attrs, err := request.Credentials[i].AttributeList(client.Configuration, getMetadataVersion(request.GetVersion())) attrs, err := request.Credentials[i].AttributeList(client.Configuration, irma.GetMetadataVersion(request.GetVersion()))
if err != nil { if err != nil {
return err return err
} }
......
...@@ -189,12 +189,28 @@ func TestLogging(t *testing.T) { ...@@ -189,12 +189,28 @@ func TestLogging(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "testip", sessionjwt.(*irma.IdentityProviderJwt).ServerName) require.Equal(t, "testip", sessionjwt.(*irma.IdentityProviderJwt).ServerName)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, entry.Disclosed) require.NotNil(t, entry.IssueCommitment)
require.NotEmpty(t, entry.Received) disclosed, err := entry.GetDisclosedCredentials(client.Configuration)
response, err := entry.GetResponse()
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, response) require.NotEmpty(t, disclosed)
require.IsType(t, &gabi.IssueCommitmentMessage{}, response)
jwt = getDisclosureJwt("testsp", irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID"))
sessionHelper(t, jwt, "verification", client)
logs, err = client.Logs()
require.NoError(t, err)
require.True(t, len(logs) == oldLogLength+2)
entry = logs[len(logs)-1]
require.NotNil(t, entry)
sessionjwt, err = entry.Jwt()
require.NoError(t, err)
require.Equal(t, "testsp", sessionjwt.(*irma.ServiceProviderJwt).ServerName)
require.NoError(t, err)
require.NotNil(t, entry.ProofList)
disclosed, err = entry.GetDisclosedCredentials(client.Configuration)
require.NoError(t, err)
require.NotEmpty(t, disclosed)
test.ClearTestStorage(t) test.ClearTestStorage(t)
} }
......
...@@ -2,19 +2,10 @@ package irmaclient ...@@ -2,19 +2,10 @@ package irmaclient
import ( import (
"encoding/json" "encoding/json"
"math/big"
"github.com/mhe/gabi" "github.com/mhe/gabi"
) )
// TODO remove on protocol upgrade
type logSessionInfo struct {
Jwt string `json:"jwt"`
Nonce *big.Int `json:"nonce"`
Context *big.Int `json:"context"`
Keys map[string]int `json:"keys"`
}
func (pki *publicKeyIdentifier) MarshalJSON() ([]byte, error) { func (pki *publicKeyIdentifier) MarshalJSON() ([]byte, error) {
temp := struct { temp := struct {
Issuer map[string]string `json:"issuer"` Issuer map[string]string `json:"issuer"`
......
package irmaclient package irmaclient
import ( import (
"encoding/json"
"time" "time"
"github.com/bwesterb/go-atum"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/mhe/gabi" "github.com/mhe/gabi"
"github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago"
) )
// LogSessionInfo is a SessionInfo alias to bypass the custom JSON marshaler
type LogSessionInfo irma.SessionInfo
// LogEntry is a log entry of a past event. // LogEntry is a log entry of a past event.
type LogEntry struct { type LogEntry struct {
// General info // General info
Type irma.Action Type irma.Action
Time irma.Timestamp // Time at which the session was completed Time irma.Timestamp // Time at which the session was completed
SessionInfo *irma.SessionInfo // Message that started the session SessionInfo *LogSessionInfo `json:",omitempty"` // Message that started the session
Version *irma.ProtocolVersion `json:",omitempty"` // Protocol version that was used in the session
// Session type-specific info // Session type-specific info
Disclosed map[irma.CredentialTypeIdentifier]map[int]irma.TranslatedString // Any session type Removed map[irma.CredentialTypeIdentifier][]irma.TranslatedString `json:",omitempty"` // In case of credential removal
Received map[irma.CredentialTypeIdentifier][]irma.TranslatedString // In case of issuance session SignedMessage []byte `json:",omitempty"` // In case of signature sessions
Removed map[irma.CredentialTypeIdentifier][]irma.TranslatedString // In case of credential removal Timestamp *atum.Timestamp `json:",omitempty"` // In case of signature sessions
SignedMessage []byte // In case of signature sessions
response interface{} // Our response (ProofList or IssueCommitmentMessage) IssueCommitment *gabi.IssueCommitmentMessage `json:",omitempty"`
rawResponse json.RawMessage // Unparsed []byte version of response ProofList gabi.ProofList `json:",omitempty"`
} }
const actionRemoval = irma.Action("removal") const actionRemoval = irma.Action("removal")
// GetDisclosedCredentials gets the list of disclosed credentials for a log entry
func (entry *LogEntry) GetDisclosedCredentials(conf *irma.Configuration) (irma.DisclosedCredentialList, error) {
if entry.Type == actionRemoval {
return irma.DisclosedCredentialList{}, nil
}
var proofs gabi.ProofList
if entry.Type == irma.ActionIssuing {
proofs = entry.IssueCommitment.Proofs
} else {
proofs = entry.ProofList
}
return irma.ExtractDisclosedCredentials(conf, proofs)
}
// GetIssuedCredentials gets the list of issued credentials for a log entry
func (entry *LogEntry) GetIssuedCredentials(conf *irma.Configuration) (list irma.CredentialInfoList, err error) {
if entry.Type != irma.ActionIssuing {
return irma.CredentialInfoList{}, nil
}
jwt, err := irma.ParseRequestorJwt(irma.ActionIssuing, entry.SessionInfo.Jwt)
if err != nil {
return
}
ir := jwt.IrmaSession().(*irma.IssuanceRequest)
return ir.GetCredentialInfoList(conf, entry.Version)
}
// GetSignedMessage gets the signed for a log entry
func (entry *LogEntry) GetSignedMessage() (abs *irma.IrmaSignedMessage, err error) {
if entry.Type != irma.ActionSigning {
return nil, nil
}
return &irma.IrmaSignedMessage{
Signature: entry.ProofList,
Nonce: entry.SessionInfo.Nonce,
Context: entry.SessionInfo.Context,
Message: string(entry.SignedMessage),
Timestamp: entry.Timestamp,
}, nil
}
func (session *session) createLogEntry(response interface{}) (*LogEntry, error) { func (session *session) createLogEntry(response interface{}) (*LogEntry, error) {
entry := &LogEntry{ entry := &LogEntry{
Type: session.Action, Type: session.Action,
Time: irma.Timestamp(time.Now()), Time: irma.Timestamp(time.Now()),
SessionInfo: session.info, Version: session.Version,
response: response, SessionInfo: (*LogSessionInfo)(session.info),
} }
// Populate session type-specific fields of the log entry (except for .Disclosed which is handled below)
var prooflist gabi.ProofList
var ok bool
switch entry.Type { switch entry.Type {
case actionRemoval:
case irma.ActionSigning: case irma.ActionSigning:
if session.IsInteractive() { // Get the signed message and timestamp
entry.SignedMessage = []byte(session.jwt.(*irma.SignatureRequestorJwt).Request.Request.Message) request := session.irmaSession.(*irma.SignatureRequest)
} else { entry.SignedMessage = []byte(request.Message)
request, ok := session.irmaSession.(*irma.SignatureRequest) entry.Timestamp = request.Timestamp
if !ok {
return nil, errors.New("Session does not contain a valid Signature Request")
}
entry.SignedMessage = []byte(request.Message)
}
fallthrough fallthrough
case irma.ActionDisclosing: case irma.ActionDisclosing:
if prooflist, ok = response.(gabi.ProofList); !ok { entry.ProofList = response.(gabi.ProofList)
return nil, errors.New("Response was not a ProofList")
}
case irma.ActionIssuing: case irma.ActionIssuing:
if entry.Received == nil { entry.IssueCommitment = response.(*gabi.IssueCommitmentMessage)
entry.Received = map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
}
for _, req := range session.jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials {
list, err := req.AttributeList(session.client.Configuration, getMetadataVersion(session.Version))
if err != nil {
continue // TODO?
}
entry.Received[list.CredentialType().Identifier()] = list.Strings()
}
var msg *gabi.IssueCommitmentMessage
if msg, ok = response.(*gabi.IssueCommitmentMessage); ok {
prooflist = msg.Proofs
} else {
return nil, errors.New("Response was not a *IssueCommitmentMessage")
}
default: default:
return nil, errors.New("Invalid log type") return nil, errors.New("Invalid log type")
} }
// Populate the list of disclosed attributes .Disclosed
for _, proof := range prooflist {
if proofd, isproofd := proof.(*gabi.ProofD); isproofd {
if entry.Disclosed == nil {
entry.Disclosed = map[irma.CredentialTypeIdentifier]map[int]irma.TranslatedString{}
}
meta := irma.MetadataFromInt(proofd.ADisclosed[1], session.client.Configuration)
id := meta.CredentialType().Identifier()
entry.Disclosed[id] = map[int]irma.TranslatedString{}
for i, attr := range proofd.ADisclosed {
if i == 1 {
continue
}
val := string(attr.Bytes())
entry.Disclosed[id][i] = irma.TranslatedString{"en": val, "nl": val}
}
}
}
return entry, nil return entry, nil
} }
...@@ -103,110 +106,3 @@ func (session *session) createLogEntry(response interface{}) (*LogEntry, error) ...@@ -103,110 +106,3 @@ func (session *session) createLogEntry(response interface{}) (*LogEntry, error)
func (entry *LogEntry) Jwt() (irma.RequestorJwt, error) { func (entry *LogEntry) Jwt() (irma.RequestorJwt, error) {
return irma.ParseRequestorJwt(entry.Type, entry.SessionInfo.Jwt) return irma.ParseRequestorJwt(entry.Type, entry.SessionInfo.Jwt)
} }
// GetResponse returns our response to the requestor from the log entry.
func (entry *LogEntry) GetResponse() (interface{}, error) {
if entry.response == nil {
switch entry.Type {
case actionRemoval:
return nil, nil
case irma.ActionSigning:
fallthrough
case irma.ActionDisclosing:
entry.response = []*gabi.ProofD{}
case irma.ActionIssuing:
entry.response = &gabi.IssueCommitmentMessage{}
default:
return nil, errors.New("Invalid log type")
}
err := json.Unmarshal(entry.rawResponse, entry.response)
if err != nil {
return nil, err
}
}
return entry.response, nil
}
type jsonLogEntry struct {
Type irma.Action
Time irma.Timestamp
SessionInfo *logSessionInfo
Disclosed map[irma.CredentialTypeIdentifier]map[int]irma.TranslatedString `json:",omitempty"`
Received map[irma.CredentialTypeIdentifier][]irma.TranslatedString `json:",omitempty"`
Removed map[irma.CredentialTypeIdentifier][]irma.TranslatedString `json:",omitempty"`
SignedMessage []byte `json:",omitempty"`
Response json.RawMessage
}
// UnmarshalJSON implements json.Unmarshaler.
func (entry *LogEntry) UnmarshalJSON(bytes []byte) error {
var err error
temp := &jsonLogEntry{}
if err = json.Unmarshal(bytes, temp); err != nil {
return err
}
*entry = LogEntry{
Type: temp.Type,
Time: temp.Time,
SessionInfo: &irma.SessionInfo{
Jwt: temp.SessionInfo.Jwt,
Nonce: temp.SessionInfo.Nonce,
Context: temp.SessionInfo.Context,
Keys: make(map[irma.IssuerIdentifier]int),
},
Removed: temp.Removed,
Disclosed: temp.Disclosed,
Received: temp.Received,
SignedMessage: temp.SignedMessage,
rawResponse: temp.Response,
}
// TODO remove on protocol upgrade
for iss, count := range temp.SessionInfo.Keys {
entry.SessionInfo.Keys[irma.NewIssuerIdentifier(iss)] = count
}
return nil
}
// MarshalJSON implements json.Marshaler.
func (entry *LogEntry) MarshalJSON() ([]byte, error) {
// If the entry was created using createLogEntry(), then entry.rawResponse == nil
if len(entry.rawResponse) == 0 && entry.response != nil {
if bytes, err := json.Marshal(entry.response); err == nil {
entry.rawResponse = json.RawMessage(bytes)
} else {
return nil, err
}
}
var si *logSessionInfo
if entry.SessionInfo != nil {
si = &logSessionInfo{
Jwt: entry.SessionInfo.Jwt,
Nonce: entry.SessionInfo.Nonce,
Context: entry.SessionInfo.Context,
Keys: make(map[string]int),
}
// TODO remove on protocol upgrade
for iss, count := range entry.SessionInfo.Keys {
si.Keys[iss.String()] = count
}
}
temp := &jsonLogEntry{
Type: entry.Type,
Time: entry.Time,
Response: entry.rawResponse,
SessionInfo: si,
Removed: entry.Removed,
Disclosed: entry.Disclosed,
Received: entry.Received,
SignedMessage: entry.SignedMessage,
}
return json.Marshal(temp)
}
...@@ -51,15 +51,6 @@ type SessionDismisser interface { ...@@ -51,15 +51,6 @@ type SessionDismisser interface {
Dismiss() Dismiss()
} }
// getMetadataVersion maps a chosen protocol version to a metadata version that
// the server will use.
func getMetadataVersion(v *irma.ProtocolVersion) byte {
if v.Below(2, 3) {
return 0x02 // no support for optional attributes
}
return 0x03 // current version
}
type session struct { type session struct {
Action irma.Action Action irma.Action
Handler Handler Handler Handler
...@@ -342,13 +333,14 @@ func (session *session) start() { ...@@ -342,13 +333,14 @@ func (session *session) start() {
if session.Action == irma.ActionIssuing { if session.Action == irma.ActionIssuing {
ir := session.irmaSession.(*irma.IssuanceRequest) ir := session.irmaSession.(*irma.IssuanceRequest)
_, err := ir.GetCredentialInfoList(session.client.Configuration, session.Version)
if err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownCredentialType, Err: err})
return
}
// Calculate singleton credentials to be removed
for _, credreq := range ir.Credentials { for _, credreq := range ir.Credentials {
info, err := credreq.Info(session.client.Configuration, getMetadataVersion(session.Version))
if err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownCredentialType, Err: err})
return
}
ir.CredentialInfoList = append(ir.CredentialInfoList, info)
preexistingCredentials := session.client.attrs(*credreq.CredentialTypeID) preexistingCredentials := session.client.attrs(*credreq.CredentialTypeID)
if len(preexistingCredentials) != 0 && preexistingCredentials[0].IsValid() && preexistingCredentials[0].CredentialType().IsSingleton { if len(preexistingCredentials) != 0 && preexistingCredentials[0].IsValid() && preexistingCredentials[0].CredentialType().IsSingleton {
ir.RemovalCredentialInfoList = append(ir.RemovalCredentialInfoList, preexistingCredentials[0].Info()) ir.RemovalCredentialInfoList = append(ir.RemovalCredentialInfoList, preexistingCredentials[0].Info())
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"html" "html"
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"regexp"
"time" "time"
"github.com/go-errors/errors" "github.com/go-errors/errors"
...@@ -73,6 +74,38 @@ var clientUpdates = []func(client *Client) error{ ...@@ -73,6 +74,38 @@ var clientUpdates = []func(client *Client) error{
// Remove the test scheme manager which was erroneously included in a production build // Remove the test scheme manager which was erroneously included in a production build
nil, // No longer necessary, also broke many unit tests nil, // No longer necessary, also broke many unit tests
// Guess and include version protocol in issuance logs
func(client *Client) (err error) {
logs, err := client.Logs()
if err != nil {
return
}
for _, log := range logs {
if log.Type != irma.ActionIssuing {
continue
}
// Ugly hack alert: unfortunately the protocol version that was used in the session is nowhere recorded.
// This means that we cannot be sure whether or not we should byteshift the presence bit out of the attributes
// that was introduced in version 2.3 of the protocol. The only thing that I can think of to determine this
// is to check if the attributes are human-readable, i.e., alphanumeric: if the presence bit is present and
// we do not shift it away, then they almost certainly will not be.
var jwt irma.RequestorJwt
jwt, err = log.Jwt()
if err != nil {
return
}
for _, attr := range jwt.IrmaSession().(*irma.IssuanceRequest).Credentials[0].Attributes {
if regexp.MustCompile("^\\w").Match([]byte(attr)) {
log.Version = irma.NewVersion(2, 2)
} else {
log.Version = irma.NewVersion(2, 3)
}
break
}
}
return client.storage.StoreLogs(logs)
},
} }
// update performs any function from clientUpdates that has not // update performs any function from clientUpdates that has not
......
...@@ -293,7 +293,7 @@ func TestVerifyValidSig(t *testing.T) { ...@@ -293,7 +293,7 @@ func TestVerifyValidSig(t *testing.T) {
attributeList := sigProofResult.ToAttributeResultList() attributeList := sigProofResult.ToAttributeResultList()
require.Len(t, attributeList, 1) require.Len(t, attributeList, 1)
require.Equal(t, attributeList[0].AttributeProofStatus, PRESENT) require.Equal(t, attributeList[0].AttributeProofStatus, PRESENT)
require.Equal(t, attributeList[0].AttributeValue, "456") require.Equal(t, attributeList[0].AttributeValue["en"], "456")
// Test if we can verify it with a request that contains strings instead of ints for nonce and context // Test if we can verify it with a request that contains strings instead of ints for nonce and context
stringRequest := "{\"nonce\": \"42\", \"context\": \"1337\", \"message\":\"I owe you everything\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}" stringRequest := "{\"nonce\": \"42\", \"context\": \"1337\", \"message\":\"I owe you everything\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}"
...@@ -310,7 +310,7 @@ func TestVerifyValidSig(t *testing.T) { ...@@ -310,7 +310,7 @@ func TestVerifyValidSig(t *testing.T) {
stringAttributeList := sigProofResult.ToAttributeResultList() stringAttributeList := sigProofResult.ToAttributeResultList()
require.Len(t, stringAttributeList, 1) require.Len(t, stringAttributeList, 1)
require.Equal(t, stringAttributeList[0].AttributeProofStatus, PRESENT) require.Equal(t, stringAttributeList[0].AttributeProofStatus, PRESENT)
require.Equal(t, stringAttributeList[0].AttributeValue, "456") require.Equal(t, stringAttributeList[0].AttributeValue["en"], "456")