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

Merge protocol into irmago

parent 0bdde251
......@@ -282,3 +282,47 @@ func TestTimestamp(t *testing.T) {
require.NoError(t, json.Unmarshal(bytes, &timestruct))
require.Equal(t, time.Time(*timestruct.Time).Unix(), int64(1500000000))
}
func TestServiceProvider(t *testing.T) {
var spjwt ServiceProviderJwt
var spjson = `{
"sprequest": {
"validity": 60,
"timeout": 60,
"request": {
"content": [
{
"label": "ID",
"attributes": ["irma-demo.RU.studentCard.studentID"]
}
]
}
}
}`
require.NoError(t, json.Unmarshal([]byte(spjson), &spjwt))
require.NotNil(t, spjwt.Request.Request.Content)
require.NotEmpty(t, spjwt.Request.Request.Content)
require.NotNil(t, spjwt.Request.Request.Content[0])
require.NotEmpty(t, spjwt.Request.Request.Content[0])
require.NotNil(t, spjwt.Request.Request.Content[0].Attributes)
require.NotEmpty(t, spjwt.Request.Request.Content[0].Attributes)
require.Equal(t, spjwt.Request.Request.Content[0].Attributes[0].Name(), "studentID")
require.NotNil(t, spjwt.Request.Request.Content.Find(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)
if err != nil { // require.NoError() does not work because of the type of err
t.Fatalf("%+v\n", err)
}
}
......@@ -5,9 +5,16 @@ import (
"encoding/json"
"errors"
"fmt"
"math/big"
"strings"
)
// Status encodes the status of an IRMA session (e.g., connected).
type Status string
// Version encodes the IRMA protocol version of an IRMA session.
type Version string
// Action encodes the session type of an IRMA session (e.g., disclosing).
type Action string
......@@ -32,6 +39,51 @@ type ApiError struct {
Stacktrace string `json:"stacktrace"`
}
// Qr contains the data of an IRMA session QR (as generated by irma_js),
// suitable for NewSession().
type Qr struct {
// Server with which to perform the session
URL string `json:"u"`
// Session type (disclosing, signing, issuing)
Type Action `json:"irmaqr"`
ProtocolVersion string `json:"v"`
ProtocolMaxVersion string `json:"vmax"`
}
// A SessionInfo is the first message in the IRMA protocol (i.e., GET on the server URL),
// containing the session request info.
type SessionInfo struct {
Jwt string `json:"jwt"`
Nonce *big.Int `json:"nonce"`
Context *big.Int `json:"context"`
Keys map[IssuerIdentifier]int `json:"keys"`
}
/*
So apparently, in the old Java implementation we forgot to write a (de)serialization for the Java
equivalent of the type IssuerIdentifier. This means a Java IssuerIdentifier does not serialize to
a string, but to e.g. `{"identifier":"irma-demo.RU"}`.
This is a complex data type, so not suitable to act as keys in a JSON map. Consequentially,
Gson serializes the `json:"keys"` field not as a map, but as a list consisting of pairs where
the first item of the pair is a serialized IssuerIdentifier as above, and the second item
of the pair is the corresponding key counter from the original map.
This is a bit of a mess to have to deserialize. See below. In a future version of the protocol,
this will have to be fixed both in the Java world and here in Go.
*/
type jsonSessionInfo struct {
Jwt string `json:"jwt"`
Nonce *big.Int `json:"nonce"`
Context *big.Int `json:"context"`
Keys [][]interface{} `json:"keys"`
}
// Statuses
const (
StatusConnected = Status("connected")
StatusCommunicating = Status("communicating")
)
// Actions
const (
ActionDisclosing = Action("disclosing")
......@@ -90,3 +142,35 @@ func JwtDecode(jwt string, body interface{}) (string, error) {
}
return header.Issuer, json.Unmarshal(bodybytes, body)
}
// UnmarshalJSON unmarshals session information.
func (si *SessionInfo) UnmarshalJSON(b []byte) error {
temp := &jsonSessionInfo{}
err := json.Unmarshal(b, temp)
if err != nil {
return err
}
si.Jwt = temp.Jwt
si.Nonce = temp.Nonce
si.Context = temp.Context
si.Keys = make(map[IssuerIdentifier]int, len(temp.Keys))
for _, item := range temp.Keys {
var idmap map[string]interface{}
var idstr string
var counter float64
var ok bool
if idmap, ok = item[0].(map[string]interface{}); !ok {
return errors.New("Failed to deserialize session info")
}
if idstr, ok = idmap["identifier"].(string); !ok {
return errors.New("Failed to deserialize session info")
}
if counter, ok = item[1].(float64); !ok {
return errors.New("Failed to deserialize session info")
}
id := NewIssuerIdentifier(idstr)
si.Keys[id] = int(counter)
}
return nil
}
// Package protocol implements the IRMA protocol.
// A new IRMA session is started with the NewSession() method
package protocol
package protocol
import (
"encoding/json"
"errors"
"math/big"
"github.com/credentials/irmago"
)
// 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
// Statuses
const (
StatusConnected = Status("connected")
StatusCommunicating = Status("communicating")
)
// Actions
const (
ActionDisclosing = Action("disclosing")
ActionSigning = Action("signing")
ActionIssuing = Action("issuing")
ActionUnknown = Action("unknown")
)
// Qr contains the data of an IRMA session QR (as generated by irma_js),
// suitable for NewSession().
type Qr struct {
// Server with which to perform the session
URL string `json:"u"`
// Session type (disclosing, signing, issuing)
Type Action `json:"irmaqr"`
ProtocolVersion string `json:"v"`
ProtocolMaxVersion string `json:"vmax"`
}
// A SessionInfo is the first message in the IRMA protocol (i.e., GET on the server URL),
// containing the session request info.
type SessionInfo struct {
Jwt string `json:"jwt"`
Nonce *big.Int `json:"nonce"`
Context *big.Int `json:"context"`
Keys map[irmago.IssuerIdentifier]int `json:"keys"`
}
/*
So apparently, in the old Java implementation we forgot to write a (de)serialization for the Java
equivalent of the type IssuerIdentifier. This means a Java IssuerIdentifier does not serialize to
a string, but to e.g. `{"identifier":"irma-demo.RU"}`.
This is a complex data type, so not suitable to act as keys in a JSON map. Consequentially,
Gson serializes the `json:"keys"` field not as a map, but as a list consisting of pairs where
the first item of the pair is a serialized IssuerIdentifier as above, and the second item
of the pair is the corresponding key counter from the original map.
This is a bit of a mess to have to deserialize. See below. In a future version of the protocol,
this will have to be fixed both in the Java world and here in Go.
*/
type jsonSessionInfo struct {
Jwt string `json:"jwt"`
Nonce *big.Int `json:"nonce"`
Context *big.Int `json:"context"`
Keys [][]interface{} `json:"keys"`
}
// UnmarshalJSON unmarshals session information.
func (si *SessionInfo) UnmarshalJSON(b []byte) error {
temp := &jsonSessionInfo{}
err := json.Unmarshal(b, temp)
if err != nil {
return err
}
si.Jwt = temp.Jwt
si.Nonce = temp.Nonce
si.Context = temp.Context
si.Keys = make(map[irmago.IssuerIdentifier]int, len(temp.Keys))
for _, item := range temp.Keys {
var idmap map[string]interface{}
var idstr string
var counter float64
var ok bool
if idmap, ok = item[0].(map[string]interface{}); !ok {
return errors.New("Failed to deserialize session info")
}
if idstr, ok = idmap["identifier"].(string); !ok {
return errors.New("Failed to deserialize session info")
}
if counter, ok = item[1].(float64); !ok {
return errors.New("Failed to deserialize session info")
}
id := irmago.NewIssuerIdentifier(idstr)
si.Keys[id] = int(counter)
}
return nil
}
package protocol
import (
"encoding/json"
"testing"
"github.com/credentials/irmago"
"github.com/stretchr/testify/require"
)
func TestServiceProvider(t *testing.T) {
var spjwt ServiceProviderJwt
var spjson = `{
"sprequest": {
"validity": 60,
"timeout": 60,
"request": {
"content": [
{
"label": "ID",
"attributes": ["irma-demo.RU.studentCard.studentID"]
}
]
}
}
}`
require.NoError(t, json.Unmarshal([]byte(spjson), &spjwt))
require.NotNil(t, spjwt.Request.Request.Content)
require.NotEmpty(t, spjwt.Request.Request.Content)
require.NotNil(t, spjwt.Request.Request.Content[0])
require.NotEmpty(t, spjwt.Request.Request.Content[0])
require.NotNil(t, spjwt.Request.Request.Content[0].Attributes)
require.NotEmpty(t, spjwt.Request.Request.Content[0].Attributes)
require.Equal(t, spjwt.Request.Request.Content[0].Attributes[0].Name(), "studentID")
require.NotNil(t, spjwt.Request.Request.Content.Find(irmago.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")))
}
func TestTransport(t *testing.T) {
transport := irmago.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)
if err != nil { // require.NoError() does not work because of the type of err
t.Fatalf("%+v\n", err)
}
}
package protocol
import (
"time"
"github.com/credentials/irmago"
)
// ServerJwt contains standard JWT fields.
type ServerJwt struct {
Type string `json:"sub"`
ServerName string `json:"iss"`
IssuedAt irmago.Timestamp `json:"iat"`
}
// A ServiceProviderRequest contains a disclosure request.
type ServiceProviderRequest struct {
Request *irmago.DisclosureRequest `json:"request"`
}
// A SignatureRequestorRequest contains a signing request.
type SignatureRequestorRequest struct {
Request *irmago.SignatureRequest `json:"request"`
}
// An IdentityProviderRequest contains an issuance request.
type IdentityProviderRequest struct {
Request *irmago.IssuanceRequest `json:"request"`
}
// ServiceProviderJwt is a requestor JWT for a disclosure session.
type ServiceProviderJwt struct {
ServerJwt
Request ServiceProviderRequest `json:"sprequest"`
}
// SignatureRequestorJwt is a requestor JWT for a signing session.
type SignatureRequestorJwt struct {
ServerJwt
Request SignatureRequestorRequest `json:"absrequest"`
}
// IdentityProviderJwt is a requestor JWT for issuance session.
type IdentityProviderJwt struct {
ServerJwt
Request IdentityProviderRequest `json:"iprequest"`
}
// NewServiceProviderJwt returns a new ServiceProviderJwt.
func NewServiceProviderJwt(servername string, dr *irmago.DisclosureRequest) *ServiceProviderJwt {
return &ServiceProviderJwt{
ServerJwt: ServerJwt{
ServerName: servername,
IssuedAt: irmago.Timestamp(time.Now()),
Type: "verification_request",
},
Request: ServiceProviderRequest{Request: dr},
}
}
// NewSignatureRequestorJwt returns a new SignatureRequestorJwt.
func NewSignatureRequestorJwt(servername string, sr *irmago.SignatureRequest) *SignatureRequestorJwt {
return &SignatureRequestorJwt{
ServerJwt: ServerJwt{
ServerName: servername,
IssuedAt: irmago.Timestamp(time.Now()),
Type: "signature_request",
},
Request: SignatureRequestorRequest{Request: sr},
}
}
// NewIdentityProviderJwt returns a new IdentityProviderJwt.
func NewIdentityProviderJwt(servername string, ir *irmago.IssuanceRequest) *IdentityProviderJwt {
return &IdentityProviderJwt{
ServerJwt: ServerJwt{
ServerName: servername,
IssuedAt: irmago.Timestamp(time.Now()),
Type: "issue_request",
},
Request: IdentityProviderRequest{Request: ir},
}
}
// A RequestorJwt contains an IRMA session object.
type RequestorJwt interface {
IrmaSession() irmago.Session
}
// IrmaSession returns an IRMA session object.
func (jwt *ServiceProviderJwt) IrmaSession() irmago.Session { return jwt.Request.Request }
// IrmaSession returns an IRMA session object.
func (jwt *SignatureRequestorJwt) IrmaSession() irmago.Session { return jwt.Request.Request }
// IrmaSession returns an IRMA session object.
func (jwt *IdentityProviderJwt) IrmaSession() irmago.Session { return jwt.Request.Request }
......@@ -60,6 +60,46 @@ type CredentialRequest struct {
Attributes map[string]string `json:"attributes"`
}
// ServerJwt contains standard JWT fields.
type ServerJwt struct {
Type string `json:"sub"`
ServerName string `json:"iss"`
IssuedAt Timestamp `json:"iat"`
}
// A ServiceProviderRequest contains a disclosure request.
type ServiceProviderRequest struct {
Request *DisclosureRequest `json:"request"`
}
// A SignatureRequestorRequest contains a signing request.
type SignatureRequestorRequest struct {
Request *SignatureRequest `json:"request"`
}
// An IdentityProviderRequest contains an issuance request.
type IdentityProviderRequest struct {
Request *IssuanceRequest `json:"request"`
}
// ServiceProviderJwt is a requestor JWT for a disclosure session.
type ServiceProviderJwt struct {
ServerJwt
Request ServiceProviderRequest `json:"sprequest"`
}
// SignatureRequestorJwt is a requestor JWT for a signing session.
type SignatureRequestorJwt struct {
ServerJwt
Request SignatureRequestorRequest `json:"absrequest"`
}
// IdentityProviderJwt is a requestor JWT for issuance session.
type IdentityProviderJwt struct {
ServerJwt
Request IdentityProviderRequest `json:"iprequest"`
}
// Timestamp is a time.Time that marshals to Unix timestamps.
type Timestamp time.Time
......@@ -224,3 +264,53 @@ func (t *Timestamp) UnmarshalJSON(b []byte) error {
*t = Timestamp(time.Unix(int64(ts), 0))
return nil
}
// NewServiceProviderJwt returns a new ServiceProviderJwt.
func NewServiceProviderJwt(servername string, dr *DisclosureRequest) *ServiceProviderJwt {
return &ServiceProviderJwt{
ServerJwt: ServerJwt{
ServerName: servername,
IssuedAt: Timestamp(time.Now()),
Type: "verification_request",
},
Request: ServiceProviderRequest{Request: dr},
}
}
// NewSignatureRequestorJwt returns a new SignatureRequestorJwt.
func NewSignatureRequestorJwt(servername string, sr *SignatureRequest) *SignatureRequestorJwt {
return &SignatureRequestorJwt{
ServerJwt: ServerJwt{
ServerName: servername,
IssuedAt: Timestamp(time.Now()),
Type: "signature_request",
},
Request: SignatureRequestorRequest{Request: sr},
}
}
// NewIdentityProviderJwt returns a new IdentityProviderJwt.
func NewIdentityProviderJwt(servername string, ir *IssuanceRequest) *IdentityProviderJwt {
return &IdentityProviderJwt{
ServerJwt: ServerJwt{
ServerName: servername,
IssuedAt: Timestamp(time.Now()),
Type: "issue_request",
},
Request: IdentityProviderRequest{Request: ir},
}
}
// A RequestorJwt contains an IRMA session object.
type RequestorJwt interface {
IrmaSession() Session
}
// IrmaSession returns an IRMA session object.
func (jwt *ServiceProviderJwt) IrmaSession() Session { return jwt.Request.Request }
// IrmaSession returns an IRMA session object.
func (jwt *SignatureRequestorJwt) IrmaSession() Session { return jwt.Request.Request }
// IrmaSession returns an IRMA session object.
func (jwt *IdentityProviderJwt) IrmaSession() Session { return jwt.Request.Request }
package protocol
package irmago
import (
"fmt"
......@@ -6,25 +6,24 @@ import (
"strconv"
"strings"
"github.com/credentials/irmago"
"github.com/mhe/gabi"
)
// PermissionHandler is a callback for providing permission for an IRMA session
// and specifying the attributes to be disclosed.
type PermissionHandler func(proceed bool, choice *irmago.DisclosureChoice)
type PermissionHandler func(proceed bool, choice *DisclosureChoice)
// A Handler contains callbacks for communication to the user.
type Handler interface {
StatusUpdate(action Action, status Status)
Success(action Action)
Cancelled(action Action)
Failure(action Action, err *irmago.Error)
UnsatisfiableRequest(action Action, missing irmago.AttributeDisjunctionList)
Failure(action Action, err *Error)
UnsatisfiableRequest(action Action, missing AttributeDisjunctionList)
AskIssuancePermission(request irmago.IssuanceRequest, ServerName string, callback PermissionHandler)
AskVerificationPermission(request irmago.DisclosureRequest, ServerName string, callback PermissionHandler)
AskSignaturePermission(request irmago.SignatureRequest, ServerName string, callback PermissionHandler)
AskIssuancePermission(request IssuanceRequest, ServerName string, callback PermissionHandler)
AskVerificationPermission(request DisclosureRequest, ServerName string, callback PermissionHandler)
AskSignaturePermission(request SignatureRequest, ServerName string, callback PermissionHandler)
AskPin(remainingAttempts int, callback func(pin string))
}
......@@ -37,9 +36,9 @@ type session struct {
Handler Handler
jwt RequestorJwt
irmaSession irmago.Session
transport *irmago.HTTPTransport
choice *irmago.DisclosureChoice
irmaSession Session
transport *HTTPTransport
choice *DisclosureChoice
}
// Supported protocol versions. Minor version numbers should be reverse sorted.
......@@ -86,7 +85,7 @@ func calcVersion(qr *Qr) (string, error) {
func NewSession(qr *Qr, handler Handler) {
version, err := calcVersion(qr)
if err != nil {
handler.Failure(ActionUnknown, &irmago.Error{ErrorCode: irmago.ErrorProtocolVersionNotSupported, Err: err})
handler.Failure(ActionUnknown, &Error{ErrorCode: ErrorProtocolVersionNotSupported, Err: err})
return
}
......@@ -95,7 +94,7 @@ func NewSession(qr *Qr, handler Handler) {
Action: Action(qr.Type),
ServerURL: qr.URL,
Handler: handler,
transport: irmago.NewHTTPTransport(qr.URL),
transport: NewHTTPTransport(qr.URL),
}
// Check if the action is one of the supported types
......@@ -106,7 +105,7 @@ func NewSession(qr *Qr, handler Handler) {
case ActionUnknown:
fallthrough
default:
handler.Failure(ActionUnknown, &irmago.Error{ErrorCode: irmago.ErrorUnknownAction, Err: nil, Info: string(session.Action)})
handler.Failure(ActionUnknown, &Error{ErrorCode: ErrorUnknownAction, Err: nil, Info: string(session.Action)})
return
}
......@@ -128,7 +127,7 @@ func (session *session) start() {
info := &SessionInfo{}
Err := session.transport.Get("jwt", info)
if Err != nil {
session.Handler.Failure(session.Action, Err.(*irmago.Error))
session.Handler.Failure(session.Action, Err.(*Error))
return
}
......@@ -142,9 +141,9 @@ func (session *session) start() {
default:
panic("Invalid session type") // does not happen, session.Action has been checked earlier
}
server, err := irmago.JwtDecode(info.Jwt, session.jwt)
server, err := JwtDecode(info.Jwt, session.jwt)
if err != nil {
session.Handler.Failure(session.Action, &irmago.Error{ErrorCode: irmago.ErrorInvalidJWT, Err: err})
session.Handler.Failure(session.Action, &Error{ErrorCode: ErrorInvalidJWT, Err: err})
return
}
session.irmaSession = session.jwt.IrmaSession()
......@@ -152,19 +151,19 @@ func (session *session) start() {
session.irmaSession.SetNonce(info.Nonce)
if session.Action == ActionIssuing {
// Store which public keys the server will use
for _, credreq := range session.irmaSession.(*irmago.IssuanceRequest).Credentials {
for _, credreq := range session.irmaSession.(*IssuanceRequest).Credentials {
credreq.KeyCounter = info.Keys[credreq.Credential.IssuerIdentifier()]
}
}
missing := irmago.Manager.CheckSatisfiability(session.irmaSession.DisjunctionList())
missing := Manager.CheckSatisfiability(session.irmaSession.DisjunctionList())
if len(missing) > 0 {
session.Handler.UnsatisfiableRequest(session.Action, missing)
return
}