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
|
// Package verify implement signature verification using minisign
package verify
import (
"fmt"
"github.com/go-errors/errors"
"github.com/jedisct1/go-minisign"
)
// 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
}
return verifyWithKeys(
signatureFileContent,
signedJSON,
expectedFileName,
minSignTime,
keyStrs,
forcePrehash,
)
}
// 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, errors.Errorf(
"invalid filename '%s'; expected 'server_list.json' or 'organization_list.json'",
filename)
}
sig, err := minisign.DecodeSignature(signatureFileContent)
if err != nil {
return false, errors.WrapPrefix(err, "invalid signature format", 0)
}
// Check if signature is prehashed, see https://jedisct1.github.io/minisign/#signature-format
if forcePrehash && sig.SignatureAlgorithm != [2]byte{'E', 'D'} {
return false, errors.Errorf(
"invalid signature algorithm '%s'; expected `ED (BLAKE2b-prehashed EdDSA)`",
sig.SignatureAlgorithm[:])
}
// 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, errors.WrapPrefix(err, fmt.Sprintf("failed to create public key '%s'", keyStr), 0)
}
if sig.KeyId != key.KeyId {
continue // Wrong key
}
valid, err := key.Verify(signedJSON, sig)
if !valid {
return false, errors.WrapPrefix(err, "invalid signature", 0)
}
// 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, errors.WrapPrefix(err, fmt.Sprintf("invalid trusted comment '%s'", sig.TrustedComment), 0)
}
if sigFileName != filename {
return false, errors.Errorf("wrong filename '%s'; expected filename '%s' for signature",
filename, sigFileName)
}
if signTime < minSignTime {
return false, errors.Errorf("sign time %d is before sign tim: %d", signTime, minSignTime)
}
return true, nil
}
// No matching allowed key found
return false, errors.Errorf("signature for filename '%s' was created with an unknown key", filename)
}
|