messages.go 8.97 KB
Newer Older
1
package irma
Sietse Ringers's avatar
Sietse Ringers committed
2
3

import (
4
	"bytes"
Sietse Ringers's avatar
Sietse Ringers committed
5
	"encoding/json"
6
	"net/url"
7
	"strconv"
Sietse Ringers's avatar
Sietse Ringers committed
8
	"strings"
Sietse Ringers's avatar
Sietse Ringers committed
9

10
11
	"fmt"

12
	"github.com/dgrijalva/jwt-go"
Sietse Ringers's avatar
Sietse Ringers committed
13
	"github.com/go-errors/errors"
14
	"github.com/privacybydesign/gabi"
Sietse Ringers's avatar
Sietse Ringers committed
15
16
)

Sietse Ringers's avatar
Sietse Ringers committed
17
18
19
// Status encodes the status of an IRMA session (e.g., connected).
type Status string

20
21
// disabled until we offer a convenient way to toggle this in irma_mobile
var ForceHttps bool = false
22

23
24
25
26
27
const (
	MinVersionHeader = "X-IRMA-MinProtocolVersion"
	MaxVersionHeader = "X-IRMA-MaxProtocolVersion"
)

28
29
// ProtocolVersion encodes the IRMA protocol version of an IRMA session.
type ProtocolVersion struct {
30
31
	Major int
	Minor int
32
33
34
35
36
37
38
}

func NewVersion(major, minor int) *ProtocolVersion {
	return &ProtocolVersion{major, minor}
}

func (v *ProtocolVersion) String() string {
39
	return fmt.Sprintf("%d.%d", v.Major, v.Minor)
40
41
}

Tomas's avatar
Tomas committed
42
43
44
func (v *ProtocolVersion) UnmarshalJSON(b []byte) (err error) {
	var str string
	if err := json.Unmarshal(b, &str); err != nil {
45
		str = string(b) // If b is not enclosed by quotes, try it directly
Tomas's avatar
Tomas committed
46
47
48
49
50
	}
	parts := strings.Split(str, ".")
	if len(parts) != 2 {
		return errors.New("Invalid protocol version number: not of form x.y")
	}
51
	if v.Major, err = strconv.Atoi(parts[0]); err != nil {
Tomas's avatar
Tomas committed
52
53
		return
	}
54
	v.Minor, err = strconv.Atoi(parts[1])
Tomas's avatar
Tomas committed
55
56
57
58
59
60
61
	return
}

func (v *ProtocolVersion) MarshalJSON() ([]byte, error) {
	return json.Marshal(v.String())
}

62
63
// Returns true if v is below the given version.
func (v *ProtocolVersion) Below(major, minor int) bool {
64
	if v.Major < major {
65
66
		return true
	}
67
	return v.Major == major && v.Minor < minor
68
}
Sietse Ringers's avatar
Sietse Ringers committed
69

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
func (v *ProtocolVersion) BelowVersion(other *ProtocolVersion) bool {
	return v.Below(other.Major, other.Minor)
}

func (v *ProtocolVersion) Above(major, minor int) bool {
	if v.Major > major {
		return true
	}
	return v.Major == major && v.Minor > minor
}

func (v *ProtocolVersion) AboveVersion(other *ProtocolVersion) bool {
	return v.Above(other.Major, other.Minor)
}

Tomas's avatar
Tomas committed
85
86
87
88
89
90
91
92
93
// 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
}

Sietse Ringers's avatar
Sietse Ringers committed
94
95
96
// Action encodes the session type of an IRMA session (e.g., disclosing).
type Action string

Tomas's avatar
Tomas committed
97
98
// ErrorType are session errors.
type ErrorType string
Sietse Ringers's avatar
Sietse Ringers committed
99

Tomas's avatar
Tomas committed
100
101
// SessionError is a protocol error.
type SessionError struct {
Sietse Ringers's avatar
Sietse Ringers committed
102
	Err error
Tomas's avatar
Tomas committed
103
	ErrorType
Tomas's avatar
Tomas committed
104
105
106
	Info         string
	RemoteError  *RemoteError
	RemoteStatus int
Sietse Ringers's avatar
Sietse Ringers committed
107
108
}

Tomas's avatar
Tomas committed
109
110
// RemoteError is an error message returned by the API server on errors.
type RemoteError struct {
111
112
113
114
115
	Status      int    `json:"status,omitempty"`
	ErrorName   string `json:"error,omitempty"`
	Description string `json:"description,omitempty"`
	Message     string `json:"message,omitempty"`
	Stacktrace  string `json:"stacktrace,omitempty"`
Sietse Ringers's avatar
Sietse Ringers committed
116
117
}

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
type Validator interface {
	Validate() error
}

// UnmarshalValidate json.Unmarshal's data, and validates it using the
// Validate() method if dest implements the Validator interface.
func UnmarshalValidate(data []byte, dest interface{}) error {
	if err := json.Unmarshal(data, dest); err != nil {
		return err
	}
	if v, ok := dest.(Validator); ok {
		return v.Validate()
	}
	return nil
}

Tomas's avatar
Tomas committed
134
func (err *RemoteError) Error() string {
135
136
137
138
139
140
141
	var msg string
	if err.Message != "" {
		msg = fmt.Sprintf(" (%s)", err.Message)
	}
	return fmt.Sprintf("%s%s: %s", err.ErrorName, msg, err.Description)
}

Sietse Ringers's avatar
Sietse Ringers committed
142
143
144
145
146
147
// 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)
Sietse Ringers's avatar
Sietse Ringers committed
148
	Type Action `json:"irmaqr"`
Sietse Ringers's avatar
Sietse Ringers committed
149
150
}

151
152
type SchemeManagerRequest Qr

Sietse Ringers's avatar
Sietse Ringers committed
153
154
155
156
// Statuses
const (
	StatusConnected     = Status("connected")
	StatusCommunicating = Status("communicating")
157
	StatusManualStarted = Status("manualStarted")
Sietse Ringers's avatar
Sietse Ringers committed
158
159
)

Sietse Ringers's avatar
Sietse Ringers committed
160
161
// Actions
const (
162
163
164
165
	ActionSchemeManager = Action("schememanager")
	ActionDisclosing    = Action("disclosing")
	ActionSigning       = Action("signing")
	ActionIssuing       = Action("issuing")
166
	ActionRedirect      = Action("redirect")
167
	ActionUnknown       = Action("unknown")
Sietse Ringers's avatar
Sietse Ringers committed
168
169
170
171
172
)

// Protocol errors
const (
	// Protocol version not supported
Tomas's avatar
Tomas committed
173
	ErrorProtocolVersionNotSupported = ErrorType("protocolVersionNotSupported")
Sietse Ringers's avatar
Sietse Ringers committed
174
	// Error in HTTP communication
Tomas's avatar
Tomas committed
175
	ErrorTransport = ErrorType("transport")
Sietse Ringers's avatar
Sietse Ringers committed
176
	// Invalid client JWT in first IRMA message
Tomas's avatar
Tomas committed
177
	ErrorInvalidJWT = ErrorType("invalidJwt")
Sietse Ringers's avatar
Sietse Ringers committed
178
	// Unkown session type (not disclosing, signing, or issuing)
Tomas's avatar
Tomas committed
179
	ErrorUnknownAction = ErrorType("unknownAction")
Sietse Ringers's avatar
Sietse Ringers committed
180
	// Crypto error during calculation of our response (second IRMA message)
Tomas's avatar
Tomas committed
181
	ErrorCrypto = ErrorType("crypto")
Sietse Ringers's avatar
Sietse Ringers committed
182
	// Server rejected our response (second IRMA message)
Tomas's avatar
Tomas committed
183
	ErrorRejected = ErrorType("rejected")
Sietse Ringers's avatar
Sietse Ringers committed
184
	// (De)serializing of a message failed
Tomas's avatar
Tomas committed
185
	ErrorSerialization = ErrorType("serialization")
Sietse Ringers's avatar
Sietse Ringers committed
186
	// Error in keyshare protocol
Tomas's avatar
Tomas committed
187
	ErrorKeyshare = ErrorType("keyshare")
188
	// API server error
Tomas's avatar
Tomas committed
189
	ErrorApi = ErrorType("api")
190
	// Server returned unexpected or malformed response
Tomas's avatar
Tomas committed
191
	ErrorServerResponse = ErrorType("serverResponse")
192
	// Credential type not present in our Configuration
193
	ErrorUnknownIdentifier = ErrorType("unknownIdentifier")
194
	// Error during downloading of credential type, issuer, or public keys
195
	ErrorConfigurationDownload = ErrorType("configurationDownload")
196
197
	// IRMA requests refers to unknown scheme manager
	ErrorUnknownSchemeManager = ErrorType("unknownSchemeManager")
198
199
	// A session is requested involving a scheme manager that has some problem
	ErrorInvalidSchemeManager = ErrorType("invalidSchemeManager")
200
201
	// Invalid session request
	ErrorInvalidRequest = ErrorType("invalidRequest")
202
203
	// Recovered panic
	ErrorPanic = ErrorType("panic")
Sietse Ringers's avatar
Sietse Ringers committed
204
205
)

206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
type Disclosure struct {
	Proofs  gabi.ProofList            `json:"proofs"`
	Indices DisclosedAttributeIndices `json:"indices"`
}

// DisclosedAttributeIndices contains, for each conjunction of an attribute disclosure request,
// a list of attribute indices, pointing to where the disclosed attributes for that conjunction
// can be found within a gabi.ProofList.
type DisclosedAttributeIndices [][]*DisclosedAttributeIndex

// DisclosedAttributeIndex points to a specific attribute in a gabi.ProofList.
type DisclosedAttributeIndex struct {
	CredentialIndex int                  `json:"cred"`
	AttributeIndex  int                  `json:"attr"`
	Identifier      CredentialIdentifier `json:"-"` // credential from which this attribute was disclosed
}

type IssueCommitmentMessage struct {
	*gabi.IssueCommitmentMessage
	Indices DisclosedAttributeIndices `json:"indices"`
}

228
229
230
231
func (err ErrorType) Error() string {
	return string(err)
}

Tomas's avatar
Tomas committed
232
func (e *SessionError) Error() string {
233
234
235
236
237
238
239
240
	var buffer bytes.Buffer
	typ := e.ErrorType
	if typ == "" {
		typ = ErrorType("unknown")
	}

	buffer.WriteString("Error type: ")
	buffer.WriteString(string(typ))
Sietse Ringers's avatar
Sietse Ringers committed
241
	if e.Err != nil {
242
243
		buffer.WriteString("\nDescription: ")
		buffer.WriteString(e.Err.Error())
Sietse Ringers's avatar
Sietse Ringers committed
244
	}
Tomas's avatar
Tomas committed
245
246
	if e.RemoteStatus != 200 {
		buffer.WriteString("\nStatus code: ")
247
		buffer.WriteString(strconv.Itoa(e.RemoteStatus))
248
	}
Tomas's avatar
Tomas committed
249
	if e.RemoteError != nil {
250
		buffer.WriteString("\nIRMA server error: ")
Tomas's avatar
Tomas committed
251
		buffer.WriteString(e.RemoteError.Error())
252
253
254
	}

	return buffer.String()
Sietse Ringers's avatar
Sietse Ringers committed
255
256
}

Tomas's avatar
Tomas committed
257
258
259
260
261
262
263
264
func (e *SessionError) WrappedError() string {
	if e.Err == nil {
		return ""
	}

	return e.Err.Error()
}

265
266
267
268
269
270
271
272
func (e *SessionError) Stack() string {
	if withStack, ok := e.Err.(*errors.Error); ok {
		return string(withStack.Stack())
	}

	return ""
}

273
274
275
276
277
278
279
func (i *IssueCommitmentMessage) Disclosure() *Disclosure {
	return &Disclosure{
		Proofs:  i.Proofs,
		Indices: i.Indices,
	}
}

280
281
282
// ParseRequestorJwt parses the specified JWT and returns the contents.
// Note: this function does not verify the signature! Do that elsewhere.
func ParseRequestorJwt(action string, requestorJwt string) (RequestorJwt, error) {
Sietse Ringers's avatar
Sietse Ringers committed
283
284
	var retval RequestorJwt
	switch action {
Sietse Ringers's avatar
Sietse Ringers committed
285
	case "verification_request", string(ActionDisclosing):
Sietse Ringers's avatar
Sietse Ringers committed
286
		retval = &ServiceProviderJwt{}
Sietse Ringers's avatar
Sietse Ringers committed
287
	case "signature_request", string(ActionSigning):
Sietse Ringers's avatar
Sietse Ringers committed
288
		retval = &SignatureRequestorJwt{}
Sietse Ringers's avatar
Sietse Ringers committed
289
	case "issue_request", string(ActionIssuing):
Sietse Ringers's avatar
Sietse Ringers committed
290
291
		retval = &IdentityProviderJwt{}
	default:
292
		return nil, errors.New("Invalid session type")
Sietse Ringers's avatar
Sietse Ringers committed
293
	}
294
	if _, _, err := new(jwt.Parser).ParseUnverified(requestorJwt, retval); err != nil {
295
		return nil, err
Sietse Ringers's avatar
Sietse Ringers committed
296
	}
297
	return retval, nil
Sietse Ringers's avatar
Sietse Ringers committed
298
}
299

300
func (qr *Qr) Validate() (err error) {
301
302
303
	if qr.URL == "" {
		return errors.New("No URL specified")
	}
304
305
	var u *url.URL
	if u, err = url.ParseRequestURI(qr.URL); err != nil {
306
307
		return errors.Errorf("Invalid URL: %s", err.Error())
	}
308
309
310
	if ForceHttps && u.Scheme != "https" {
		return errors.Errorf("URL did not begin with https")
	}
311
312
313
314
315

	switch qr.Type {
	case ActionDisclosing: // nop
	case ActionIssuing: // nop
	case ActionSigning: // nop
316
	case ActionRedirect: // nop
317
318
319
320
321
322
	default:
		return errors.New("Unsupported session type")
	}

	return nil
}
323
324
325
326
327
328
329
330
331
332
333
334
335

func (smr *SchemeManagerRequest) Validate() error {
	if smr.Type != ActionSchemeManager {
		return errors.New("Not a scheme manager request")
	}
	if smr.URL == "" {
		return errors.New("No URL specified")
	}
	if _, err := url.ParseRequestURI(smr.URL); err != nil {
		return errors.Errorf("Invalid URL: %s", err.Error())
	}
	return nil
}