/
auth.go
284 lines (237 loc) · 8.2 KB
/
auth.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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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
273
274
275
276
277
278
279
280
281
282
283
284
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2020 Mitchell Wasson
// This file is part of Weaklayer Gateway.
// Weaklayer Gateway is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package auth
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"fmt"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/pbkdf2"
)
// KeyJSONSchema should be used to validate keys in JSON form
const KeyJSONSchema = `
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Key",
"type": "object",
"properties": {
"group": {
"type": "string",
"format": "uuid",
"example": "73d0710f-c4a4-468a-9087-a06073bebe8c",
"description": "The sensor group that this key enables installation into."
},
"secret": {
"type": "string",
"pattern": "^[a-zA-z0-9+\/]{86}==$",
"example": "4QIuEulkopN+QbyMXOMVCKZBps0JjutFpI7U1OI6FgUFY6587Be9xLRPUSOK7fajM+RokZSwv3F31t5XAbTXnQ==",
"description": "A 512-bit secret value that acts as a password for installing a sensor."
},
"checksum": {
"type": "string",
"pattern": "^[a-zA-z0-9+\/]{43}=$",
"example": "iNg+ovQngoycxTRsb3byxjuE26E5eUQChVmmh54e2To=",
"description": "A 256-bit hash of the other fields to determine if mistakes were made when entering a key."
}
}
}
`
// Key is a value that administrators will configure the Weaklayer Sensor with.
// Its purpose is to authenticate the sensor to the gateway so we know it
// is allowed to send data.
type Key struct {
Group uuid.UUID `json:"group"`
Secret []byte `json:"secret"`
Checksum []byte `json:"checksum"`
}
// Verifier is data provided to the Weaklayer Gateway so it can verify the key
// that a Weaklayer Sensor Provides.
type Verifier struct {
Group uuid.UUID `json:"group"`
Salt []byte `json:"salt"`
Hash []byte `json:"hash"`
Checksum []byte `json:"checksum"`
}
// VerifierJSONSchema should be used to validate verifiers in JSON form
const VerifierJSONSchema = `
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Verifier",
"type": "object",
"properties": {
"group": {
"type": "string",
"format": "uuid",
"example": "73d0710f-c4a4-468a-9087-a06073bebe8c",
"description": "The sensor group that this verifier enables installation into."
},
"salt": {
"type": "string",
"pattern": "^[a-zA-z0-9+\/]{6}==$",
"example": "SGQ7Fw==",
"description": "A 32-bit random value used in the calculation of the install key secret hash."
},
"hash": {
"type": "string",
"pattern": "^[a-zA-z0-9+\/]{43}=$",
"example": "BioOJy7op91gsmlO+L9PMAsRrUWNKP5JJQZ6JwCjlk4=",
"description": "A 256-bit hash of the install key secret and the validator salt. Note this is a much more expensive hash is used here than for the checksum."
},
"checksum": {
"type": "string",
"pattern": "^[a-zA-z0-9+\/]{43}=$",
"example": "jDmzy23i1Db7on0hTLZTsnATlGg79QOwwHZprlmSJ18=",
"description": "A hash of the other fields to determine if mistakes were made when entering a validator."
}
}
}
`
// NewKey creates a new install key with a random secret for the provided group
// On error, the default Key is returned with error set
func NewKey(group uuid.UUID) (Key, error) {
var key Key
secret, err := NewRandomBytes(64)
if err != nil {
return key, fmt.Errorf("Install key secret generation failed: %w", err)
}
checksum, err := calculateKeyChecksum(group, secret)
if err != nil {
return key, fmt.Errorf("Install key checksum calculation failed: %w", err)
}
key = Key{
Group: group,
Secret: secret,
Checksum: checksum,
}
return key, nil
}
// NewVerifier creates a new install verifier from an existing key and a random salt
// On error, the default Verifier is returned with error set
func NewVerifier(key Key) (Verifier, error) {
var verifier Verifier
// 32-bit salt according to NIST reccomendations
salt, err := NewRandomBytes(4)
if err != nil {
return verifier, fmt.Errorf("Install verifier salt generation failed: %w", err)
}
hash := calculateHash(key, salt)
sum, err := calculateVerifierChecksum(key.Group, salt, hash)
if err != nil {
return verifier, fmt.Errorf("Install verifier checksum calculation failed: %w", err)
}
verifier = Verifier{
Group: key.Group,
Salt: salt,
Hash: hash,
Checksum: sum,
}
return verifier, nil
}
// Verify ensures the following:
// - GroupIDs match
// - Key Checksum valid
// - Verifier Checksum valid
// - Key secret matches Verifier hash
//
// Returns true if all these conditions are met and no errors. False otherwise.
func Verify(key Key, verifier Verifier) bool {
return UUIDEquals(key.Group, verifier.Group) && isKeyChecksumValid(key) && IsVerifierValid(verifier) && isHashValid(key, verifier)
}
// IsVerifierValid ensures the verifier's checksum matches
func IsVerifierValid(verifier Verifier) bool {
calculatedChecksum, err := calculateVerifierChecksum(verifier.Group, verifier.Salt, verifier.Hash)
if err != nil {
log.Info().Err(err).Msg("Install verifier checksum calculation failed")
return false
}
return bytes.Equal(calculatedChecksum, verifier.Checksum)
}
// UUIDEquals returns true if the two UUID contents are equal. False otherwise.
func UUIDEquals(u1 uuid.UUID, u2 uuid.UUID) bool {
u1Bytes, err := u1.MarshalBinary()
if err != nil {
log.Warn().Err(err).Msg("Failed to marshal UUID to bytes")
return false
}
u2Bytes, err := u2.MarshalBinary()
if err != nil {
log.Warn().Err(err).Msg("Failed to marshal UUID to bytes")
return false
}
return bytes.Equal(u1Bytes, u2Bytes)
}
// CalculateHash produces an install key hash given the salt and install key
func calculateHash(key Key, Salt []byte) []byte {
// 10000 iterations according to NIST reccomendations
return pbkdf2.Key(key.Secret, Salt, 10000, 32, sha256.New)
}
func isHashValid(key Key, verifier Verifier) bool {
calculatedHash := calculateHash(key, verifier.Salt)
return bytes.Equal(verifier.Hash, calculatedHash)
}
func calculateChecksum(input []byte) ([]byte, error) {
digest := sha256.New()
b, err := digest.Write(input)
if err != nil || b != len(input) {
return nil, fmt.Errorf("Error calculating sha256 digest: %w", err)
}
return digest.Sum(nil), nil
}
func calculateVerifierChecksum(group uuid.UUID, salt []byte, hash []byte) ([]byte, error) {
uuidBytes, err := group.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("Failed to marshal UUID to bytes: %w", err)
}
sumInput := append(uuidBytes, salt...)
sumInput = append(sumInput, hash...)
sum, err := calculateChecksum(sumInput)
if err != nil {
return nil, fmt.Errorf("Failed to calculate verifier checksum: %w", err)
}
return sum, nil
}
func calculateKeyChecksum(group uuid.UUID, secret []byte) ([]byte, error) {
uuidBytes, err := group.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("Failed to marshal UUID to bytes: %w", err)
}
sumInput := append(uuidBytes, secret...)
sum, err := calculateChecksum(sumInput)
if err != nil {
return nil, fmt.Errorf("Failed to calculate key checksum: %w", err)
}
return sum, nil
}
func isKeyChecksumValid(key Key) bool {
calculatedChecksum, err := calculateKeyChecksum(key.Group, key.Secret)
if err != nil {
log.Warn().Err(err).Msg("Key checksum validation failed")
return false
}
return bytes.Equal(calculatedChecksum, key.Checksum)
}
// NewRandomBytes generates a byte array full of cryptographic strength random data
func NewRandomBytes(length int) ([]byte, error) {
retVal := make([]byte, length)
numBytes, err := rand.Read(retVal)
if err != nil {
return nil, fmt.Errorf("Random byte generation failed: %w", err)
}
if numBytes != length {
return nil, fmt.Errorf("Failed to generate enough random bytes: %w", err)
}
return retVal, nil
}