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
|
package verify
import (
"fmt"
"github.com/jedisct1/go-minisign"
"github.com/eduvpn/eduvpn-common/types"
)
// Verify verifies the signature (.minisig file format) on signedJson.
//
// expectedFileName must be set to the file type to be verified, either "server_list.json" or "organization_list.json".
// minSign must be set to the minimum UNIX timestamp (without milliseconds) for the file version.
// This value should not be smaller than the time on the previous document verified.
// forcePrehash indicates whether or not we want to force the use of prehashed signatures
// In the future we want to remove this parameter and only allow prehashed signatures
//
// The return value will either be (true, nil) for a valid signature or (false, VerifyError) otherwise.
//
// Verify is a wrapper around verifyWithKeys where allowedPublicKeys is set to the list from https://git.sr.ht/~eduvpn/disco.eduvpn.org#public-keys.
func Verify(
signatureFileContent string,
signedJson []byte,
expectedFileName string,
minSignTime uint64,
forcePrehash bool,
) (bool, error) {
// keys taken from https://git.sr.ht/~eduvpn/disco.eduvpn.org#public-keys
keyStrs := []string{
"RWRtBSX1alxyGX+Xn3LuZnWUT0w//B6EmTJvgaAxBMYzlQeI+jdrO6KF", // fkooman@tuxed.net, kolla@uninett.no
"RWQKqtqvd0R7rUDp0rWzbtYPA3towPWcLDCl7eY9pBMMI/ohCmrS0WiM", // RoSp
}
valid, err := verifyWithKeys(
signatureFileContent,
signedJson,
expectedFileName,
minSignTime,
keyStrs,
forcePrehash,
)
if err != nil {
return valid, &types.WrappedErrorMessage{Message: "failed signature verify", Err: err}
}
return valid, nil
}
// verifyWithKeys verifies the Minisign signature in signatureFileContent (minisig file format) over the server_list/organization_list JSON in signedJson.
//
// Verification is performed using a matching key in allowedPublicKeys.
// The signature is checked to be a Ed25519 Minisign (optionally Ed25519 Blake2b-512 prehashed, see forcePrehash) signature with a valid trusted comment.
// The file type that is verified is indicated by expectedFileName, which must be one of "server_list.json"/"organization_list.json".
// The trusted comment is checked to be of the form "timestamp:<timestamp>\tfile:<expectedFileName>", optionally suffixed by something, e.g. "\thashed".
// The signature is checked to have a timestamp with a value of at least minSignTime, which is a UNIX timestamp without milliseconds.
//
// The return value will either be (true, nil) on success or (false, detailedVerifyError) on failure.
// Note that every error path is wrapped in a custom type here because minisign does not return custom error types, they use errors.New
func verifyWithKeys(
signatureFileContent string,
signedJson []byte,
filename string,
minSignTime uint64,
allowedPublicKeys []string,
forcePrehash bool,
) (bool, error) {
switch filename {
case "server_list.json", "organization_list.json":
break
default:
return false, &VerifyUnknownExpectedFilenameError{
Filename: filename,
Expected: "server_list.json or organization_list.json",
}
}
sig, err := minisign.DecodeSignature(signatureFileContent)
if err != nil {
return false, &VerifyInvalidSignatureFormatError{Err: err}
}
// Check if signature is prehashed, see https://jedisct1.github.io/minisign/#signature-format
if forcePrehash && sig.SignatureAlgorithm != [2]byte{'E', 'D'} {
return false, &VerifyInvalidSignatureAlgorithmError{
Algorithm: string(sig.SignatureAlgorithm[:]),
WantedAlgorithm: "ED (BLAKE2b-prehashed EdDSA)",
}
}
// Find allowed key used for signature
for _, keyStr := range allowedPublicKeys {
key, err := minisign.NewPublicKey(keyStr)
if err != nil {
// Should only happen if Verify is wrong or extraKey is invalid
return false, &VerifyCreatePublicKeyError{PublicKey: keyStr, Err: err}
}
if sig.KeyId != key.KeyId {
continue // Wrong key
}
valid, err := key.Verify(signedJson, sig)
if !valid {
return false, &VerifyInvalidSignatureError{Err: err}
}
// Parse trusted comment
var signTime uint64
var sigFileName string
// sigFileName cannot have spaces
_, err = fmt.Sscanf(
sig.TrustedComment,
"trusted comment: timestamp:%d\tfile:%s",
&signTime,
&sigFileName,
)
if err != nil {
return false, &VerifyInvalidTrustedCommentError{
TrustedComment: sig.TrustedComment,
Err: err,
}
}
if sigFileName != filename {
return false, &VerifyWrongSigFilenameError{Filename: filename, SigFilename: sigFileName}
}
if signTime < minSignTime {
return false, &VerifySigTimeEarlierError{SigTime: signTime, MinSigTime: minSignTime}
}
return true, nil
}
// No matching allowed key found
return false, &VerifyUnknownKeyError{Filename: filename}
}
type VerifyUnknownExpectedFilenameError struct {
Filename string
Expected string
}
func (e *VerifyUnknownExpectedFilenameError) Error() string {
return fmt.Sprintf("invalid filename: %s, expected: %s", e.Filename, e.Expected)
}
type VerifyInvalidSignatureFormatError struct {
Err error
}
func (e *VerifyInvalidSignatureFormatError) Error() string {
return fmt.Sprintf("invalid signature format with error: %v", e.Err)
}
func (e *VerifyInvalidSignatureFormatError) Unwrap() error {
return e.Err
}
type VerifyInvalidSignatureAlgorithmError struct {
Algorithm string
WantedAlgorithm string
}
func (e *VerifyInvalidSignatureAlgorithmError) Error() string {
return fmt.Sprintf(
"invalid signature algorithm: %s, wanted: %s",
e.Algorithm,
e.WantedAlgorithm,
)
}
type VerifyCreatePublicKeyError struct {
PublicKey string
Err error
}
func (e *VerifyCreatePublicKeyError) Error() string {
return fmt.Sprintf("failed to create public key: %s with error: %v", e.PublicKey, e.Err)
}
func (e *VerifyCreatePublicKeyError) Unwrap() error {
return e.Err
}
type VerifyInvalidSignatureError struct {
Err error
}
func (e *VerifyInvalidSignatureError) Error() string {
return fmt.Sprintf("invalid signature with error: %v", e.Err)
}
func (e *VerifyInvalidSignatureError) Unwrap() error {
return e.Err
}
type VerifyInvalidTrustedCommentError struct {
TrustedComment string
Err error
}
func (e *VerifyInvalidTrustedCommentError) Error() string {
return fmt.Sprintf("invalid trusted comment: %s with error: %v", e.TrustedComment, e.Err)
}
func (e *VerifyInvalidTrustedCommentError) Unwrap() error {
return e.Err
}
type VerifyWrongSigFilenameError struct {
Filename string
SigFilename string
}
func (e *VerifyWrongSigFilenameError) Error() string {
return fmt.Sprintf(
"wrong filename: %s, expected filename: %s for signature",
e.Filename,
e.SigFilename,
)
}
type VerifySigTimeEarlierError struct {
SigTime uint64
MinSigTime uint64
}
func (e *VerifySigTimeEarlierError) Error() string {
return fmt.Sprintf("Sign time: %d is earlier than sign time: %d", e.SigTime, e.MinSigTime)
}
type VerifyUnknownKeyError struct {
Filename string
}
func (e *VerifyUnknownKeyError) Error() string {
return fmt.Sprintf("signature for filename: %s was created with an unknown key", e.Filename)
}
|