/
transaction.go
142 lines (124 loc) · 4.19 KB
/
transaction.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package sqrl
import (
"errors"
"net/url"
"strings"
)
var (
// ErrInvalidClient the 'client' parameter is invalid.
ErrInvalidClient = errors.New("invalid client param")
// ErrInvalidClient the 'server' parameter is invalid.
ErrInvalidServer = errors.New("invalid server param")
// ErrInvalidIDSig the identity signature parameter is not correct
// for the given identity key and payload.
ErrInvalidIDSig = errors.New("invalid identity signature")
// ErrIPMismatch the client IP address does not match the original
// transaction in the negotiation.
ErrIPMismatch = errors.New("ip does not match")
)
// Request represents a (usually HTTP) request sent from
// a SQRL client to a SQRL server.
type Request struct {
Nut Nut
Client string
Server string
Ids Signature
// TODO: Pids
// TODO: Suk
ClientIP string
}
// Transaction represents a SQRL request and response, a single
// exchange in a SQRL negotiation. There are likely to be multiple
// transactions for a single SQRL sign-in.
//
// Next is the Nut that was issued to the client as part of this
// transaction and should be resubmitted with the next request to
// continue where the client and server left off.
type Transaction struct {
Next Nut
*Request
}
// Verify checks that a request from a SQRL client is valid.
//
// The transaction that started this session should be provided if one exists.
// If no previous transaction is provided, the request is presumed to be the
// first request for this session.
//
// Note: No attempt is made to verify the previous transaction (other than
// to compare it's properties to those of the new transaction). It is assumed
// that the previous transaction has already had it's signatures checked and
// payload validated.
//
// If a validation error is encoutered, the precise error will be returned and the
// correct transaction information flags will be set on the response.
func Verify(req *Request, first *Transaction, response *ServerMsg) (*ClientMsg, error) {
if req.ClientIP == "" {
// ClientIP MUST always be set correctly for same-device protections
// to work correctly. We do not return an exported error here, because
// this is not an error that should ever be caught in practise. It is
// a development mistake and we check for it here to catch accidental
// misuse that may result in a security vulnerability.
return nil, errors.New("client ip should never be empty")
}
client, errc := ParseClient(req.Client)
if errc != nil {
response.Tif = response.Tif | TIFCommandFailed | TIFClientFailure
return nil, ErrInvalidClient
}
serverOK := verifyServer(req.Server, first)
if !serverOK {
response.Tif = response.Tif | TIFCommandFailed | TIFClientFailure
return nil, ErrInvalidServer
}
signedPayload := req.Client + req.Server
if !req.Ids.Verify(client.Idk, signedPayload) {
response.Tif = response.Tif | TIFCommandFailed | TIFClientFailure
return nil, ErrInvalidIDSig
}
if first == nil {
return client, nil
}
// TODO: Do we set IP Match for the first request? Presume not
if first.ClientIP == req.ClientIP {
response.Set(TIFIPMatch)
}
ipMustMatch := !client.HasOpt(OptNoIPTest)
ipsMatch := response.Is(TIFIPMatch)
if ipMustMatch && !ipsMatch {
return nil, ErrIPMismatch
}
// TODO: Verify IDK Match
// TODO: Is cmd "ident" allowed if there is no previous transaction?
return client, nil
}
func verifyServer(serverRaw string, first *Transaction) bool {
bytes, err := Base64.DecodeString(serverRaw)
if err != nil {
return false
}
server := string(bytes)
if strings.HasPrefix(server, "sqrl") {
if first != nil {
// Providing the previous query URL as the server
// param is ONLY valid for the first transaction
return false
}
serverURL, err := url.Parse(server)
if err != nil {
return false
}
// TODO: Assert URL matches server configuration
// eg. domain, "server friendly name", etc.
nut := serverURL.Query().Get("nut")
return nut != ""
} else {
// TODO: We must be able to do something smarter with this
// We should be able to check the previous server val we sent
// to the client and compare this against that.
msg, err := ParseServer(serverRaw)
if err != nil || msg == nil || msg.Nut == "" {
return false
}
return true
}
}