summaryrefslogtreecommitdiff
path: root/verify.go
blob: 336ba73990fb349b4fe5cf36837bf441b8d004c3 (plain)
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
package eduvpn_discovery

import (
	"fmt"
	"github.com/jedisct1/go-minisign"
	"os"
)

// getKeys returns keys taken from https://git.sr.ht/~eduvpn/disco.eduvpn.org#public-keys.
func getKeys() []string {
	return []string{
		"RWRtBSX1alxyGX+Xn3LuZnWUT0w//B6EmTJvgaAxBMYzlQeI+jdrO6KF", // fkooman@tuxed.net, kolla@uninett.no
		"RWQKqtqvd0R7rUDp0rWzbtYPA3towPWcLDCl7eY9pBMMI/ohCmrS0WiM", // RoSp
	}
}

// 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.
//
// 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) (bool, error) {
	keyStrs := getKeys()
	if extraKey != "" {
		keyStrs = append(keyStrs, extraKey)
		_, err := fmt.Fprintf(os.Stderr, "INSECURE TEST MODE ENABLED WITH KEY %q\n", extraKey)
		if err != nil {
			panic(err)
		}
	}
	valid, err := verifyWithKeys(signatureFileContent, signedJson, expectedFileName, minSignTime, keyStrs)
	if err != nil {
		if err.(detailedVerifyError).Code == errInvalidPublicKey {
			panic(err) // This should not happen unless keyStrs has an invalid key
		}
		return valid, err.(detailedVerifyError).ToVerifyError()
	}
	return valid, nil
}

// extraKey is an extra allowed key for testing.
var extraKey = ""

// InsecureTestingSetExtraKey adds an extra allowed key for verification with Verify.
// ONLY USE FOR TESTING. Applies to all threads. Probably not thread-safe. Do not call in parallel to Verify.
//
// keyString must be a Base64-encoded Minisign key, or empty to reset.
func InsecureTestingSetExtraKey(keyString string) {
	extraKey = keyString
}

// VerifyErrorCode Simplified error code for public interface.
type VerifyErrorCode int8

const (
	ErrUnknownExpectedFileName    VerifyErrorCode = iota + 1 // Unknown expected file name specified. The signature has not been verified.
	ErrInvalidSignature                                      // Signature is invalid (for the expected file type).
	ErrInvalidSignatureUnknownKey                            // Signature was created with an unknown key and has not been verified.
	ErrTooOld                                                // Signature timestamp smaller than specified minimum signing time (rollback).
)

type VerifyError struct {
	Code     VerifyErrorCode
	Detailed detailedVerifyError
}

func (err VerifyError) Error() string {
	return err.Detailed.Error()
}
func (err VerifyError) Unwrap() error {
	return err.Detailed
}

// 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 Blake2b-prehashed Ed25519 Minisign 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.
func verifyWithKeys(signatureFileContent string, signedJson []byte, expectedFileName string, minSignTime uint64, allowedPublicKeys []string) (bool, error) {
	switch expectedFileName {
	case "server_list.json", "organization_list.json":
		break
	default:
		return false, detailedVerifyError{errUnknownExpectedFileName, "invalid expected file name", nil}
	}

	sig, err := minisign.DecodeSignature(signatureFileContent)
	if err != nil {
		return false, detailedVerifyError{errInvalidSignatureFormat, "invalid signature format", err}
	}

	// Check if signature is prehashed, see https://jedisct1.github.io/minisign/#signature-format
	if sig.SignatureAlgorithm != [2]byte{'E', 'D'} {
		return false, detailedVerifyError{errInvalidSignatureAlgorithm, "BLAKE2b-prehashed EdDSA signature required", nil}
	}

	// 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, detailedVerifyError{errInvalidPublicKey, "internal error: could not create public key", err}
		}

		if sig.KeyId != key.KeyId {
			continue // Wrong key
		}

		valid, err := key.Verify(signedJson, sig)
		if !valid {
			return false, detailedVerifyError{errInvalidSignature, "invalid signature", 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, detailedVerifyError{errInvalidTrustedComment, "failed to interpret trusted comment", err}
		}

		if sigFileName != expectedFileName {
			return false, detailedVerifyError{errWrongFileName, "signature was created for wrong file", nil}
		}

		if signTime < minSignTime {
			return false, detailedVerifyError{errTooOld, "signature was created a time earlier than the minimum time specified", nil}
		}

		return true, nil
	}

	// No matching allowed key found
	return false, detailedVerifyError{errWrongKey, "signature was created with an unknown key", nil}
}

// detailedVerifyErrorCode used for unit tests.
type detailedVerifyErrorCode int8

const (
	errUnknownExpectedFileName detailedVerifyErrorCode = iota + 1
	errInvalidSignatureFormat
	errInvalidSignatureAlgorithm
	errInvalidPublicKey
	errInvalidSignature
	errInvalidTrustedComment
	errWrongFileName
	errTooOld
	errWrongKey
)

func (code detailedVerifyErrorCode) ToVerifyErrorCode() VerifyErrorCode {
	switch code {
	case errUnknownExpectedFileName:
		return ErrUnknownExpectedFileName
	case errInvalidSignatureFormat:
		return ErrInvalidSignature
	case errInvalidSignatureAlgorithm:
		return ErrInvalidSignature
	case errInvalidPublicKey:
		panic("errInvalidPublicKey cannot be converted to VerifyErrorCode")
	case errInvalidSignature:
		return ErrInvalidSignature
	case errInvalidTrustedComment:
		return ErrInvalidSignature
	case errWrongFileName:
		return ErrInvalidSignature
	case errTooOld:
		return ErrTooOld
	case errWrongKey:
		return ErrInvalidSignatureUnknownKey
	}
	panic("invalid detailedVerifyErrorCode")
}

type detailedVerifyError struct {
	Code    detailedVerifyErrorCode
	Message string
	Cause   error
}

func (err detailedVerifyError) Error() string {
	return err.Message
}
func (err detailedVerifyError) Unwrap() error {
	return err.Cause
}

func (err detailedVerifyError) ToVerifyError() VerifyError {
	return VerifyError{err.Code.ToVerifyErrorCode(), err}
}