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 (
)
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"`
AttributeProofStatus AttributeProofStatus `json:"status"`
}
......@@ -105,12 +105,20 @@ func (al *AttributeList) Strings() []TranslatedString {
if val == nil {
continue
}
al.strings[i] = map[string]string{"en": *val, "nl": *val} // TODO
al.strings[i] = translateAttribute(val)
}
}
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 {
attr := al.Ints[i+1]
metadataVersion := al.MetadataAttribute.Version()
......@@ -340,7 +348,7 @@ type AttributeDisjunction struct {
type DisclosedAttributeDisjunction struct {
AttributeDisjunction
DisclosedValue string
DisclosedValue TranslatedString
DisclosedId AttributeTypeIdentifier
ProofStatus AttributeProofStatus
}
......@@ -464,7 +472,7 @@ func (disclosedAttributeDisjunction *DisclosedAttributeDisjunction) MarshalJSON(
Label string `json:"label"`
Attributes []AttributeTypeIdentifier `json:"attributes"`
DisclosedValue string `json:"disclosedValue"`
DisclosedValue TranslatedString `json:"disclosedValue"`
DisclosedId AttributeTypeIdentifier `json:"disclosedId"`
ProofStatus AttributeProofStatus `json:"proofStatus"`
}{
......
......@@ -144,12 +144,11 @@ func (id IssuerIdentifier) MarshalText() ([]byte, error) {
return []byte(id.String()), nil
}
// TODO enable this when updating protocol
//// UnmarshalText implements encoding.TextUnmarshaler.
//func (id *IssuerIdentifier) UnmarshalText(text []byte) error {
// *id = NewIssuerIdentifier(string(text))
// return nil
//}
// UnmarshalText implements encoding.TextUnmarshaler.
func (id *IssuerIdentifier) UnmarshalText(text []byte) error {
*id = NewIssuerIdentifier(string(text))
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (id CredentialTypeIdentifier) MarshalText() ([]byte, error) {
......
......@@ -630,7 +630,7 @@ func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, re
// we save none of them to fail the session cleanly
gabicreds := []*gabi.Credential{}
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 {
return err
}
......
......@@ -189,12 +189,28 @@ func TestLogging(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "testip", sessionjwt.(*irma.IdentityProviderJwt).ServerName)
require.NoError(t, err)
require.NotEmpty(t, entry.Disclosed)
require.NotEmpty(t, entry.Received)
response, err := entry.GetResponse()
require.NotNil(t, entry.IssueCommitment)
disclosed, err := entry.GetDisclosedCredentials(client.Configuration)
require.NoError(t, err)
require.NotNil(t, response)
require.IsType(t, &gabi.IssueCommitmentMessage{}, response)
require.NotEmpty(t, disclosed)
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)
}
......
......@@ -2,19 +2,10 @@ package irmaclient
import (
"encoding/json"
"math/big"
"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) {
temp := struct {
Issuer map[string]string `json:"issuer"`
......
package irmaclient
import (
"encoding/json"
"time"
"github.com/bwesterb/go-atum"
"github.com/go-errors/errors"
"github.com/mhe/gabi"
"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.
type LogEntry struct {
// General info
Type irma.Action
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
Disclosed map[irma.CredentialTypeIdentifier]map[int]irma.TranslatedString // Any session type
Received map[irma.CredentialTypeIdentifier][]irma.TranslatedString // In case of issuance session
Removed map[irma.CredentialTypeIdentifier][]irma.TranslatedString // In case of credential removal
SignedMessage []byte // In case of signature sessions
Removed map[irma.CredentialTypeIdentifier][]irma.TranslatedString `json:",omitempty"` // In case of credential removal
SignedMessage []byte `json:",omitempty"` // In case of signature sessions
Timestamp *atum.Timestamp `json:",omitempty"` // In case of signature sessions
response interface{} // Our response (ProofList or IssueCommitmentMessage)
rawResponse json.RawMessage // Unparsed []byte version of response
IssueCommitment *gabi.IssueCommitmentMessage `json:",omitempty"`
ProofList gabi.ProofList `json:",omitempty"`
}
const actionRemoval = irma.Action("removal")
func (session *session) createLogEntry(response interface{}) (*LogEntry, error) {
entry := &LogEntry{
Type: session.Action,
Time: irma.Timestamp(time.Now()),
SessionInfo: session.info,
response: response,
// 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
}
// 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 {
case irma.ActionSigning:
if session.IsInteractive() {
entry.SignedMessage = []byte(session.jwt.(*irma.SignatureRequestorJwt).Request.Request.Message)
var proofs gabi.ProofList
if entry.Type == irma.ActionIssuing {
proofs = entry.IssueCommitment.Proofs
} else {
request, ok := session.irmaSession.(*irma.SignatureRequest)
if !ok {
return nil, errors.New("Session does not contain a valid Signature Request")
}
entry.SignedMessage = []byte(request.Message)
proofs = entry.ProofList
}
fallthrough
case irma.ActionDisclosing:
if prooflist, ok = response.(gabi.ProofList); !ok {
return nil, errors.New("Response was not a ProofList")
}
case irma.ActionIssuing:
if entry.Received == nil {
entry.Received = map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
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
}
for _, req := range session.jwt.(*irma.IdentityProviderJwt).Request.Request.Credentials {
list, err := req.AttributeList(session.client.Configuration, getMetadataVersion(session.Version))
jwt, err := irma.ParseRequestorJwt(irma.ActionIssuing, entry.SessionInfo.Jwt)
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:
return nil, errors.New("Invalid log type")
return
}
ir := jwt.IrmaSession().(*irma.IssuanceRequest)
return ir.GetCredentialInfoList(conf, entry.Version)
}
// 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}
}
}
// 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 entry, nil
return &irma.IrmaSignedMessage{
Signature: entry.ProofList,
Nonce: entry.SessionInfo.Nonce,
Context: entry.SessionInfo.Context,
Message: string(entry.SignedMessage),
Timestamp: entry.Timestamp,
}, nil
}
// Jwt returns the JWT from the requestor that started the IRMA session which the
// current log entry tracks.
func (entry *LogEntry) Jwt() (irma.RequestorJwt, error) {
return irma.ParseRequestorJwt(entry.Type, entry.SessionInfo.Jwt)
}
func (session *session) createLogEntry(response interface{}) (*LogEntry, error) {
entry := &LogEntry{
Type: session.Action,
Time: irma.Timestamp(time.Now()),
Version: session.Version,
SessionInfo: (*LogSessionInfo)(session.info),
}
// 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:
// Get the signed message and timestamp
request := session.irmaSession.(*irma.SignatureRequest)
entry.SignedMessage = []byte(request.Message)
entry.Timestamp = request.Timestamp
fallthrough
case irma.ActionDisclosing:
entry.response = []*gabi.ProofD{}
entry.ProofList = response.(gabi.ProofList)
case irma.ActionIssuing:
entry.response = &gabi.IssueCommitmentMessage{}
entry.IssueCommitment = 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
return entry, 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)
// Jwt returns the JWT from the requestor that started the IRMA session which the
// current log entry tracks.
func (entry *LogEntry) Jwt() (irma.RequestorJwt, error) {
return irma.ParseRequestorJwt(entry.Type, entry.SessionInfo.Jwt)
}
......@@ -51,15 +51,6 @@ type SessionDismisser interface {
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 {
Action irma.Action
Handler Handler
......@@ -342,13 +333,14 @@ func (session *session) start() {
if session.Action == irma.ActionIssuing {
ir := session.irmaSession.(*irma.IssuanceRequest)
for _, credreq := range ir.Credentials {
info, err := credreq.Info(session.client.Configuration, getMetadataVersion(session.Version))
_, err := ir.GetCredentialInfoList(session.client.Configuration, session.Version)
if err != nil {
session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownCredentialType, Err: err})
return
}
ir.CredentialInfoList = append(ir.CredentialInfoList, info)
// Calculate singleton credentials to be removed
for _, credreq := range ir.Credentials {
preexistingCredentials := session.client.attrs(*credreq.CredentialTypeID)
if len(preexistingCredentials) != 0 && preexistingCredentials[0].IsValid() && preexistingCredentials[0].CredentialType().IsSingleton {
ir.RemovalCredentialInfoList = append(ir.RemovalCredentialInfoList, preexistingCredentials[0].Info())
......
......@@ -6,6 +6,7 @@ import (
"html"
"io/ioutil"
"math/big"
"regexp"
"time"
"github.com/go-errors/errors"
......@@ -73,6 +74,38 @@ var clientUpdates = []func(client *Client) error{
// Remove the test scheme manager which was erroneously included in a production build
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
......
......@@ -293,7 +293,7 @@ func TestVerifyValidSig(t *testing.T) {
attributeList := sigProofResult.ToAttributeResultList()
require.Len(t, attributeList, 1)
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
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) {
stringAttributeList := sigProofResult.ToAttributeResultList()
require.Len(t, stringAttributeList, 1)
require.Equal(t, stringAttributeList[0].AttributeProofStatus, PRESENT)
require.Equal(t, stringAttributeList[0].AttributeValue, "456")
require.Equal(t, stringAttributeList[0].AttributeValue["en"], "456")
// Test verify against unmatched request (i.e. different nonce, context or message)
unmatched := "{\"nonce\": 42, \"context\": 1337, \"message\":\"I owe you NOTHING\",\"content\":[{\"label\":\"Student number (RU)\",\"attributes\":[\"irma-demo.RU.studentCard.studentID\"]}]}"
......@@ -324,7 +324,7 @@ func TestVerifyValidSig(t *testing.T) {
proofStatus, disclosed := VerifySigWithoutRequest(conf, irmaSignedMessage)
require.Equal(t, proofStatus, VALID)
require.Len(t, disclosed, 1)
require.Equal(t, *disclosed[0].Attributes[NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")], "456")
require.Equal(t, disclosed[0].Attributes[NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")]["en"], "456")
}
func TestVerifyInValidSig(t *testing.T) {
......
......@@ -31,6 +31,26 @@ func (v *ProtocolVersion) String() string {
return fmt.Sprintf("%d.%d", v.major, v.minor)
}
func (v *ProtocolVersion) UnmarshalJSON(b []byte) (err error) {
var str string
if err := json.Unmarshal(b, &str); err != nil {
return err
}
parts := strings.Split(str, ".")
if len(parts) != 2 {
return errors.New("Invalid protocol version number: not of form x.y")
}
if v.major, err = strconv.Atoi(parts[0]); err != nil {
return
}
v.minor, err = strconv.Atoi(parts[1])
return
}
func (v *ProtocolVersion) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String())
}
// Returns true if v is below the given version.
func (v *ProtocolVersion) Below(major, minor int) bool {
if v.major < major {
......@@ -39,6 +59,15 @@ func (v *ProtocolVersion) Below(major, minor int) bool {
return v.major == major && v.minor < minor
}
// GetMetadataVersion maps a chosen protocol version to a metadata version that
// the server will use.
func GetMetadataVersion(v *ProtocolVersion) byte {
if v.Below(2, 3) {
return 0x02 // no support for optional attributes
}
return 0x03 // current version
}
// Action encodes the session type of an IRMA session (e.g., disclosing).
type Action string
......
......@@ -70,6 +70,8 @@ type IssuanceRequest struct {
SessionRequest
Credentials []*CredentialRequest `json:"credentials"`
Disclose AttributeDisjunctionList `json:"disclose"`
// Derived data
CredentialInfoList CredentialInfoList `json:",omitempty"`
RemovalCredentialInfoList CredentialInfoList
}
......@@ -246,6 +248,19 @@ func (ir *IssuanceRequest) ToDisclose() AttributeDisjunctionList {
return ir.Disclose
}
func (ir *IssuanceRequest) GetCredentialInfoList(conf *Configuration, version *ProtocolVersion) (CredentialInfoList, error) {
if ir.CredentialInfoList == nil {
for _, credreq := range ir.Credentials {
info, err := credreq.Info(conf, GetMetadataVersion(version))
if err != nil {
return nil, err
}
ir.CredentialInfoList = append(ir.CredentialInfoList, info)
}
}
return ir.CredentialInfoList, nil
}
// GetContext returns the context of this session.
func (ir *IssuanceRequest) GetContext() *big.Int { return ir.Context }
......
......@@ -38,7 +38,8 @@ type SignatureProofResult struct {
// DisclosedCredential contains raw disclosed credentials, without any extra parsing information
type DisclosedCredential struct {
metadataAttribute *MetadataAttribute
Attributes map[AttributeTypeIdentifier]*string `json:"attributes"`
rawAttributes map[AttributeTypeIdentifier]*string
Attributes map[AttributeTypeIdentifier]TranslatedString `json:"attributes"`
}
type DisclosedCredentialList []*DisclosedCredential
......@@ -56,7 +57,7 @@ func (disclosed DisclosedCredentialList) isAttributeSatisfied(attributeId Attrib
disclosedAttributeValue := cred.Attributes[attributeId]
// Continue to next credential if requested attribute isn't disclosed in this credential
if disclosedAttributeValue == nil {
if disclosedAttributeValue == nil || len(disclosedAttributeValue) == 0 {
continue
}
......@@ -64,9 +65,9 @@ func (disclosed DisclosedCredentialList) isAttributeSatisfied(attributeId Attrib
// Attribute is satisfied if: