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

import (
	"encoding/base64"
	"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
12
13
	"bytes"

	"fmt"

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

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

21
22
var ForceHttps bool = true

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 {
Sietse Ringers's avatar
Sietse Ringers committed
111
112
113
114
115
116
117
	Status      int    `json:"status"`
	ErrorName   string `json:"error"`
	Description string `json:"description"`
	Message     string `json:"message"`
	Stacktrace  string `json:"stacktrace"`
}

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
166
	ActionSchemeManager = Action("schememanager")
	ActionDisclosing    = Action("disclosing")
	ActionSigning       = Action("signing")
	ActionIssuing       = Action("issuing")
	ActionUnknown       = Action("unknown")
Sietse Ringers's avatar
Sietse Ringers committed
167
168
169
170
171
)

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

Tomas's avatar
Tomas committed
203
func (e *SessionError) Error() string {
204
205
206
207
208
209
210
211
	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
212
	if e.Err != nil {
213
214
		buffer.WriteString("\nDescription: ")
		buffer.WriteString(e.Err.Error())
Sietse Ringers's avatar
Sietse Ringers committed
215
	}
Tomas's avatar
Tomas committed
216
217
	if e.RemoteStatus != 200 {
		buffer.WriteString("\nStatus code: ")
218
		buffer.WriteString(strconv.Itoa(e.RemoteStatus))
219
	}
Tomas's avatar
Tomas committed
220
	if e.RemoteError != nil {
221
		buffer.WriteString("\nIRMA server error: ")
Tomas's avatar
Tomas committed
222
		buffer.WriteString(e.RemoteError.Error())
223
224
225
	}

	return buffer.String()
Sietse Ringers's avatar
Sietse Ringers committed
226
227
}

Tomas's avatar
Tomas committed
228
229
230
231
232
233
234
235
func (e *SessionError) WrappedError() string {
	if e.Err == nil {
		return ""
	}

	return e.Err.Error()
}

236
237
238
239
240
241
242
243
func (e *SessionError) Stack() string {
	if withStack, ok := e.Err.(*errors.Error); ok {
		return string(withStack.Stack())
	}

	return ""
}

244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
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"`
}

func (i *IssueCommitmentMessage) Disclosure() *Disclosure {
	return &Disclosure{
		Proofs:  i.Proofs,
		Indices: i.Indices,
	}
}

273
func JwtDecode(jwt string, body interface{}) error {
Sietse Ringers's avatar
Sietse Ringers committed
274
275
	jwtparts := strings.Split(jwt, ".")
	if jwtparts == nil || len(jwtparts) < 2 {
276
		return errors.New("Not a JWT")
Sietse Ringers's avatar
Sietse Ringers committed
277
278
279
	}
	bodybytes, err := base64.RawStdEncoding.DecodeString(jwtparts[1])
	if err != nil {
280
		return err
Sietse Ringers's avatar
Sietse Ringers committed
281
	}
282
	return json.Unmarshal(bodybytes, body)
Sietse Ringers's avatar
Sietse Ringers committed
283
}
Sietse Ringers's avatar
Sietse Ringers committed
284

285
func ParseRequestorJwt(action string, jwt string) (RequestorJwt, error) {
Sietse Ringers's avatar
Sietse Ringers committed
286
287
	var retval RequestorJwt
	switch action {
Sietse Ringers's avatar
Sietse Ringers committed
288
	case "verification_request", string(ActionDisclosing):
Sietse Ringers's avatar
Sietse Ringers committed
289
		retval = &ServiceProviderJwt{}
Sietse Ringers's avatar
Sietse Ringers committed
290
	case "signature_request", string(ActionSigning):
Sietse Ringers's avatar
Sietse Ringers committed
291
		retval = &SignatureRequestorJwt{}
Sietse Ringers's avatar
Sietse Ringers committed
292
	case "issue_request", string(ActionIssuing):
Sietse Ringers's avatar
Sietse Ringers committed
293
294
		retval = &IdentityProviderJwt{}
	default:
295
		return nil, errors.New("Invalid session type")
Sietse Ringers's avatar
Sietse Ringers committed
296
	}
297
	err := JwtDecode(jwt, retval)
Sietse Ringers's avatar
Sietse Ringers committed
298
	if err != nil {
299
		return nil, err
Sietse Ringers's avatar
Sietse Ringers committed
300
	}
301
	return retval, nil
Sietse Ringers's avatar
Sietse Ringers committed
302
}
303

304
func (qr *Qr) Validate() (err error) {
305
306
307
	if qr.URL == "" {
		return errors.New("No URL specified")
	}
308
309
	var u *url.URL
	if u, err = url.ParseRequestURI(qr.URL); err != nil {
310
311
		return errors.Errorf("Invalid URL: %s", err.Error())
	}
312
313
314
	if ForceHttps && u.Scheme != "https" {
		return errors.Errorf("URL did not begin with https")
	}
315
316
317
318
319
320
321
322
323
324
325

	switch qr.Type {
	case ActionDisclosing: // nop
	case ActionIssuing: // nop
	case ActionSigning: // nop
	default:
		return errors.New("Unsupported session type")
	}

	return nil
}
326
327
328
329
330
331
332
333
334
335
336
337
338

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
}