summaryrefslogtreecommitdiff
path: root/verify.go
blob: 89a3d748927cef83355640b3b6ed6dcc4471c28c (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
package eduvpn_verify

import (
	"fmt"
	"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.
//
// The return value will either be (true, nil) for a valid signature or (false, err) 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 := []string{
		"RWRtBSX1alxyGX+Xn3LuZnWUT0w//B6EmTJvgaAxBMYzlQeI+jdrO6KF", // fkooman@tuxed.net, kolla@uninett.no
		"RWQKqtqvd0R7rUDp0rWzbtYPA3towPWcLDCl7eY9pBMMI/ohCmrS0WiM", // RoSp
	}
	valid, err := verifyWithKeys(signatureFileContent, signedJson, expectedFileName, minSignTime, keyStrs)
	if err != nil && err.(VerifyError).Code == ErrInvalidPublicKey {
		panic(err) // This should not happen
	}
	return valid, err
}

// 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, err) 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, VerifyError{ErrUnknownExpectedFileName,
			fmt.Sprintf("invalid expected file name (%v)", expectedFileName), nil}
	}

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

	if sig.SignatureAlgorithm != [2]byte{'E', 'D'} {
		return false, VerifyError{ErrInvalidSignatureAlgorithm, "BLAKE2b-prehashed EdDSA signature required", nil}
	}

	for _, keyStr := range allowedPublicKeys {
		key, err := minisign.NewPublicKey(keyStr)
		if err != nil {
			return false, VerifyError{ErrInvalidPublicKey, "internal error: could not create public key", err}
		}

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

		valid, err := key.Verify(signedJson, sig)
		if !valid {
			return false, VerifyError{ErrInvalidSignature, "invalid signature", err}
		}

		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, VerifyError{ErrInvalidTrustedComment,
				fmt.Sprintf("failed to interpret trusted comment (%q)", sig.TrustedComment), err}
		}

		if sigFileName != expectedFileName {
			return false, VerifyError{ErrWrongFileName,
				fmt.Sprintf("signature was on file %q instead of expected %q", sigFileName, expectedFileName), nil}
		}

		if signTime < minSignTime {
			return false, VerifyError{ErrTooOld,
				fmt.Sprintf("signature was created at %v < minimum time (%v)", signTime, minSignTime), nil}
		}

		return true, nil
	}

	return false, VerifyError{ErrWrongKey, "signature was created with an unknown key", nil}
}

type VerifyErrCode int

const (
	ErrUnknownExpectedFileName VerifyErrCode = iota
	ErrInvalidSignatureFormat
	ErrInvalidSignatureAlgorithm
	ErrInvalidPublicKey
	ErrInvalidSignature
	ErrInvalidTrustedComment
	ErrWrongFileName
	ErrTooOld
	ErrWrongKey
)

type VerifyError struct {
	Code    VerifyErrCode
	Message string
	Cause   error
}

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