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

Add first part of IRMA protocol

parent ad48738e
......@@ -3,9 +3,12 @@ package irmago
import (
"crypto/sha256"
"encoding/binary"
"errors"
"math/big"
"time"
"encoding/json"
"github.com/mhe/gabi"
)
......@@ -76,6 +79,15 @@ func (al *AttributeList) Strings() []string {
return al.strings
}
func (al *AttributeList) Attribute(identifier AttributeTypeIdentifier) string {
for i, desc := range al.CredentialType().Attributes {
if desc.ID == string(identifier.String()) {
return al.Strings()[i]
}
}
return ""
}
// MetadataFromInt wraps the given Int
func MetadataFromInt(i *big.Int) *MetadataAttribute {
return &MetadataAttribute{Int: i}
......@@ -209,3 +221,147 @@ func shortToByte(x int) []byte {
binary.BigEndian.PutUint16(bytes, uint16(x))
return bytes
}
// An AttributeDisjunction encapsulates a list of possible attributes, one
// of which should be disclosed.
type AttributeDisjunction struct {
Label string
Attributes []AttributeTypeIdentifier
Values map[AttributeTypeIdentifier]string
selected *AttributeTypeIdentifier
}
// An AttributeDisjunctionList is a list of AttributeDisjunctions.
type AttributeDisjunctionList []*AttributeDisjunction
type DisjunctionListContainer interface {
DisjunctionList() AttributeDisjunctionList
}
// HasValues indicates if the attributes of this disjunction have values
// that should be satisfied.
func (disjunction *AttributeDisjunction) HasValues() bool {
return disjunction.Values != nil && len(disjunction.Values) != 0
}
// Satisfied indicates if this disjunction has a valid chosen attribute
// to be disclosed.
func (disjunction *AttributeDisjunction) Satisfied() bool {
if disjunction.selected == nil {
return false
}
for _, attr := range disjunction.Attributes {
if *disjunction.selected == attr {
return true
}
}
return false
}
// MatchesStore returns true if all attributes contained in the disjunction are
// present in the MetaStore.
func (disjunction *AttributeDisjunction) MatchesStore() bool {
for ai := range disjunction.Values {
creddescription, exists := MetaStore.Credentials[ai.CredentialTypeIdentifier()]
if !exists {
return false
}
if !creddescription.ContainsAttribute(ai) {
return false
}
}
return true
}
// Satisfied indicates whether each contained attribute disjunction has a chosen attribute.
func (dl AttributeDisjunctionList) Satisfied() bool {
for _, disjunction := range dl {
if !disjunction.Satisfied() {
return false
}
}
return true
}
// Find searches for and returns the disjunction that contains the specified attribute identifier, or nil if not found.
func (dl AttributeDisjunctionList) Find(ai AttributeTypeIdentifier) *AttributeDisjunction {
for _, disjunction := range dl {
for _, attr := range disjunction.Attributes {
if attr == ai {
return disjunction
}
}
}
return nil
}
// MarshalJSON marshals the disjunction to JSON.
func (disjunction *AttributeDisjunction) MarshalJSON() ([]byte, error) {
if !disjunction.HasValues() {
temp := struct {
Label string `json:"label"`
Attributes []AttributeTypeIdentifier `json:"attributes"`
}{
Label: disjunction.Label,
Attributes: disjunction.Attributes,
}
return json.Marshal(temp)
}
temp := struct {
Label string `json:"label"`
Attributes map[AttributeTypeIdentifier]string `json:"attributes"`
}{
Label: disjunction.Label,
Attributes: disjunction.Values,
}
return json.Marshal(temp)
}
// UnmarshalJSON unmarshals an attribute disjunction from JSON.
func (disjunction *AttributeDisjunction) UnmarshalJSON(bytes []byte) error {
if disjunction.Values == nil {
disjunction.Values = make(map[AttributeTypeIdentifier]string)
}
if disjunction.Attributes == nil {
disjunction.Attributes = make([]AttributeTypeIdentifier, 0, 3)
}
// We don't know if the json element "attributes" is a list, or a map.
// So we unmarshal it into a temporary struct that has interface{} as the
// type of "attributes", so that we can check which of the two it is.
temp := struct {
Label string `json:"label"`
Attributes interface{} `json:"attributes"`
}{}
json.Unmarshal(bytes, &temp)
disjunction.Label = temp.Label
switch temp.Attributes.(type) {
case map[string]interface{}:
temp := struct {
Label string `json:"label"`
Attributes map[string]string `json:"attributes"`
}{}
json.Unmarshal(bytes, &temp)
for str, value := range temp.Attributes {
id := NewAttributeTypeIdentifier(str)
disjunction.Attributes = append(disjunction.Attributes, id)
disjunction.Values[id] = value
}
case []interface{}:
temp := struct {
Label string `json:"label"`
Attributes []string `json:"attributes"`
}{}
json.Unmarshal(bytes, &temp)
for _, str := range temp.Attributes {
disjunction.Attributes = append(disjunction.Attributes, NewAttributeTypeIdentifier(str))
}
default:
return errors.New("could not parse attribute disjunction: element 'attributes' was incorrect")
}
return nil
}
......@@ -24,6 +24,18 @@ type AttributeTypeIdentifier struct {
metaObjectIdentifier
}
type CredentialIdentifier struct {
Type CredentialTypeIdentifier
Index int
Count int
}
type AttributeIdentifier struct {
Type AttributeTypeIdentifier
Index int
Count int
}
func (oi metaObjectIdentifier) Parent() string {
str := string(oi)
return str[:strings.LastIndex(str, ".")]
......@@ -72,3 +84,7 @@ func (id CredentialTypeIdentifier) IssuerIdentifier() IssuerIdentifier {
func (id AttributeTypeIdentifier) CredentialTypeIdentifier() CredentialTypeIdentifier {
return NewCredentialTypeIdentifier(id.Parent())
}
func (id AttributeTypeIdentifier) IsCredential() bool {
return strings.Count(id.String(), ".") == 2
}
......@@ -7,6 +7,8 @@ import (
"testing"
"time"
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
......@@ -155,3 +157,45 @@ func TestMetadataCompatibility(t *testing.T) {
teardown(t)
}
func TestAttributeDisjunctionMarshaling(t *testing.T) {
disjunction := AttributeDisjunction{}
var _ json.Unmarshaler = &disjunction
var _ json.Marshaler = &disjunction
id := NewAttributeTypeIdentifier("MijnOverheid.ageLower.over18")
attrsjson := `
{
"label": "Over 18",
"attributes": {
"MijnOverheid.ageLower.over18": "yes",
"Thalia.age.over18": "Yes"
}
}`
require.NoError(t, json.Unmarshal([]byte(attrsjson), &disjunction))
require.True(t, disjunction.HasValues())
require.Contains(t, disjunction.Attributes, id)
require.Contains(t, disjunction.Values, id)
require.Equal(t, disjunction.Values[id], "yes")
disjunction = AttributeDisjunction{}
attrsjson = `
{
"label": "Over 18",
"attributes": [
"MijnOverheid.ageLower.over18",
"Thalia.age.over18"
]
}`
require.NoError(t, json.Unmarshal([]byte(attrsjson), &disjunction))
require.False(t, disjunction.HasValues())
require.Contains(t, disjunction.Attributes, id)
require.True(t, disjunction.MatchesStore())
require.False(t, disjunction.Satisfied())
disjunction.selected = &disjunction.Attributes[0]
require.True(t, disjunction.Satisfied())
}
......@@ -199,3 +199,47 @@ func (cm *CredentialManager) Add(cred *Credential) (err error) {
err = cm.storeAttributes()
return
}
func (cm *CredentialManager) Candidates(disjunction *AttributeDisjunction) []*AttributeIdentifier {
candidates := make([]*AttributeIdentifier, 10)
for _, attribute := range disjunction.Attributes {
credId := attribute.CredentialTypeIdentifier()
if !MetaStore.Contains(credId) {
continue
}
creds := cm.credentials[credId]
count := len(creds)
if count == 0 {
continue
}
for i, cred := range creds {
id := &AttributeIdentifier{Type: attribute, Index: i, Count: count}
if attribute.IsCredential() {
candidates = append(candidates, id)
} else {
attrs := NewAttributeListFromInts(cred.Attributes[1:])
val := attrs.Attribute(attribute)
if val == "" {
continue
}
if !disjunction.HasValues() || val == disjunction.Values[attribute] {
candidates = append(candidates, id)
}
}
}
}
return candidates
}
func (cm *CredentialManager) CheckSatisfiability(disjunctions DisjunctionListContainer) AttributeDisjunctionList {
missing := make(AttributeDisjunctionList, 5)
for _, disjunction := range disjunctions.DisjunctionList() {
if len(cm.Candidates(disjunction)) == 0 {
missing = append(missing, disjunction)
}
}
return missing
}
package protocol
import (
"encoding/json"
"errors"
"github.com/credentials/irmago"
)
// An AttributeDisjunction encapsulates a list of possible attributes, one
// of which should be disclused.
type AttributeDisjunction struct {
Label string
Attributes []irmago.AttributeTypeIdentifier
Values map[irmago.AttributeTypeIdentifier]string
selected *irmago.AttributeTypeIdentifier
}
// An AttributeDisjunctionList is a list of AttributeDisjunctions.
type AttributeDisjunctionList []*AttributeDisjunction
// HasValues indicates if the attributes of this disjunction have values
// that should be satisfied.
func (disjunction *AttributeDisjunction) HasValues() bool {
return disjunction.Values != nil && len(disjunction.Values) != 0
}
// Satisfied indicates if this disjunction has a valid chosen attribute
// to be disclosed.
func (disjunction *AttributeDisjunction) Satisfied() bool {
if disjunction.selected == nil {
return false
}
for _, attr := range disjunction.Attributes {
if *disjunction.selected == attr {
return true
}
}
return false
}
// MatchesStore returns true if all attributes contained in the disjunction are
// present in the MetaStore.
func (disjunction *AttributeDisjunction) MatchesStore() bool {
for ai := range disjunction.Values {
creddescription, exists := irmago.MetaStore.Credentials[ai.CredentialTypeIdentifier()]
if !exists {
return false
}
if !creddescription.ContainsAttribute(ai) {
return false
}
}
return true
}
// Satisfied indicates whether each contained attribute disjunction has a chosen attribute.
func (dl AttributeDisjunctionList) Satisfied() bool {
for _, disjunction := range dl {
if !disjunction.Satisfied() {
return false
}
}
return true
}
// Find searches for and returns the disjunction that contains the specified attribute identifier, or nil if not found.
func (dl AttributeDisjunctionList) Find(ai irmago.AttributeTypeIdentifier) *AttributeDisjunction {
for _, disjunction := range dl {
for _, attr := range disjunction.Attributes {
if attr == ai {
return disjunction
}
}
}
return nil
}
// MarshalJSON marshals the disjunction to JSON.
func (disjunction *AttributeDisjunction) MarshalJSON() ([]byte, error) {
if !disjunction.HasValues() {
temp := struct {
Label string `json:"label"`
Attributes []irmago.AttributeTypeIdentifier `json:"attributes"`
}{
Label: disjunction.Label,
Attributes: disjunction.Attributes,
}
return json.Marshal(temp)
}
temp := struct {
Label string `json:"label"`
Attributes map[irmago.AttributeTypeIdentifier]string `json:"attributes"`
}{
Label: disjunction.Label,
Attributes: disjunction.Values,
}
return json.Marshal(temp)
}
// UnmarshalJSON unmarshals an attribute disjunction from JSON.
func (disjunction *AttributeDisjunction) UnmarshalJSON(bytes []byte) error {
if disjunction.Values == nil {
disjunction.Values = make(map[irmago.AttributeTypeIdentifier]string)
}
if disjunction.Attributes == nil {
disjunction.Attributes = make([]irmago.AttributeTypeIdentifier, 0, 3)
}
// We don't know if the json element "attributes" is a list, or a map.
// So we unmarshal it into a temporary struct that has interface{} as the
// type of "attributes", so that we can check which of the two it is.
temp := struct {
Label string `json:"label"`
Attributes interface{} `json:"attributes"`
}{}
json.Unmarshal(bytes, &temp)
disjunction.Label = temp.Label
switch temp.Attributes.(type) {
case map[string]interface{}:
temp := struct {
Label string `json:"label"`
Attributes map[string]string `json:"attributes"`
}{}
json.Unmarshal(bytes, &temp)
for str, value := range temp.Attributes {
id := irmago.NewAttributeTypeIdentifier(str)
disjunction.Attributes = append(disjunction.Attributes, id)
disjunction.Values[id] = value
}
case []interface{}:
temp := struct {
Label string `json:"label"`
Attributes []string `json:"attributes"`
}{}
json.Unmarshal(bytes, &temp)
for _, str := range temp.Attributes {
disjunction.Attributes = append(disjunction.Attributes, irmago.NewAttributeTypeIdentifier(str))
}
default:
return errors.New("could not parse attribute disjunction: element 'attributes' was incorrect")
}
return nil
}
......@@ -5,33 +5,68 @@ import (
"strconv"
"time"
"math/big"
"github.com/credentials/irmago"
)
// Session types.
// Timestamp is a time.Time that marshals to Unix timestamps.
type Timestamp time.Time
// Status encodes the status of an IRMA session (e.g., connected).
type Status string
// Action encodes the session type of an IRMA session (e.g., disclosing).
type Action string
// Version encodes the IRMA protocol version of an IRMA session.
type Version string
// SessionError are session errors.
type SessionError string
// Statuses
const (
DISCLOSING = SessionType("disclosing")
ISSUING = SessionType("issuing")
SIGNING = SessionType("signing")
StatusConnected = Status("connected")
StatusCommunicating = Status("communicating")
StatusDone = Status("done")
)
// Timestamp is a time.Time that marshals to Unix timestamps.
type Timestamp time.Time
// Actions
const (
ActionDisclosing = Action("disclosing")
ActionSigning = Action("signing")
ActionIssuing = Action("issuing")
ActionUnknown = Action("unknown")
)
// SessionType is a session type (DISCLOSING, ISSUING or SIGNING).
type SessionType string
// Protocol errors
const (
ErrorProtocolVersionNotSupported = SessionError("versionNotSupported")
ErrorInvalidURL = SessionError("invalidUrl")
ErrorTransport = SessionError("httpError")
ErrorInvalidJWT = SessionError("invalidJwt")
ErrorUnknownAction = SessionError("unknownAction")
)
// Qr contains the data of an IRMA session QR.
type Qr struct {
URL string `json:"u"`
ProtocolVersion string `json:"v"`
ProtocolMaxVersion string `json:"vmax"`
Type SessionType `json:"irmaqr"`
URL string `json:"u"`
ProtocolVersion string `json:"v"`
ProtocolMaxVersion string `json:"vmax"`
Type Action `json:"irmaqr"`
}
// A SessionInfo is the first message in the IRMA protocol.
type SessionInfo struct {
Jwt string `json:"jwt"`
Nonce *big.Int `json:"nonce"`
Context *big.Int `json:"context"`
Keys map[irmago.IssuerIdentifier]int `json:"keys"`
}
// A DisclosureChoice contains the attributes chosen to be disclosed.
type DisclosureChoice struct {
Request SessionRequest
Attributes []*irmago.AttributeIdentifier
}
......
......@@ -10,48 +10,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestAttributeDisjunctionMarshaling(t *testing.T) {
disjunction := AttributeDisjunction{}
var _ json.Unmarshaler = &disjunction
var _ json.Marshaler = &disjunction
id := irmago.NewAttributeTypeIdentifier("MijnOverheid.ageLower.over18")
attrsjson := `
{
"label": "Over 18",
"attributes": {
"MijnOverheid.ageLower.over18": "yes",
"Thalia.age.over18": "Yes"
}
}`
require.NoError(t, json.Unmarshal([]byte(attrsjson), &disjunction))
require.True(t, disjunction.HasValues())
require.Contains(t, disjunction.Attributes, id)
require.Contains(t, disjunction.Values, id)
require.Equal(t, disjunction.Values[id], "yes")
disjunction = AttributeDisjunction{}
attrsjson = `
{
"label": "Over 18",
"attributes": [
"MijnOverheid.ageLower.over18",
"Thalia.age.over18"
]
}`
require.NoError(t, json.Unmarshal([]byte(attrsjson), &disjunction))
require.False(t, disjunction.HasValues())
require.Contains(t, disjunction.Attributes, id)
require.True(t, disjunction.MatchesStore())
require.False(t, disjunction.Satisfied())
disjunction.selected = &disjunction.Attributes[0]
require.True(t, disjunction.Satisfied())
}
func TestTimestamp(t *testing.T) {
mytime := Timestamp(time.Unix(1500000000, 0))
timestruct := struct{ Time *Timestamp }{Time: &mytime}
......@@ -92,3 +50,15 @@ func TestServiceProviderRequest(t *testing.T) {
require.NotNil(t, sprequest.Request.Request.Content.Find(irmago.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")))
}
func TestTransport(t *testing.T) {
transport := NewHTTPTransport("https://xkcd.com")
obj := &struct {
Num int `json:"num"`
Img string `json:"img"`
Title string `json:"title"`
}{}
err := transport.Get("614/info.0.json", obj)
require.NoError(t, err)
}
......@@ -13,7 +13,7 @@ type SessionRequest struct {
type DisclosureRequest struct {
SessionRequest
Content AttributeDisjunctionList `json:"content"`
Content irmago.AttributeDisjunctionList `json:"content"`
}
type SignatureRequest struct {
......@@ -37,8 +37,8 @@ type ServerRequest struct {
type IssuanceRequest struct {
SessionRequest
Credentials []CredentialRequest `json:"credentials"`
Discose []*AttributeDisjunctionList `json:"disclose"`
Credentials []CredentialRequest `json:"credentials"`
Disclose irmago.AttributeDisjunctionList `json:"disclose"`
}
type ServiceProviderRequest struct {
......@@ -61,3 +61,15 @@ type IdentityProviderRequest struct {
Request IssuanceRequest `json:"request"`
} `json:"iprequest"`
}
func (spr *ServiceProviderRequest) DisjunctionList() irmago.AttributeDisjunctionList {
return spr.Request.Request.Content