diff options
| author | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-02-08 15:56:53 +0100 |
|---|---|---|
| committer | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-04-05 12:26:10 +0200 |
| commit | 70b4bad8904fe02fe4d783b75c6137ba959363ec (patch) | |
| tree | caa512596ed5accde73acb31b232d5540edfb401 /src | |
| parent | 32bd89a77c8aa2fed235291171791def04724b8d (diff) | |
Go: Begin working on abstracting server/organization list
Signed-off-by: jwijenbergh <jeroenwijenbergh@protonmail.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/server.go | 76 | ||||
| -rw-r--r-- | src/verify.go | 200 |
2 files changed, 276 insertions, 0 deletions
diff --git a/src/server.go b/src/server.go new file mode 100644 index 0000000..7973654 --- /dev/null +++ b/src/server.go @@ -0,0 +1,76 @@ +package eduvpn_discovery + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "fmt" +) + +// Struct that defines the json format for +// url: "https://disco.eduvpn.org/v2/organization_list.json" +type organizations struct { + v string `json:"v"` + OrganizationList []struct { + DisplayName struct { + En string `json:"en"` + } `json:"display_name"` + OrgId string `json:"org_id"` + SecureInternetHome string `json:"secure_internet_home"` + KeywordList struct { + En string `json:"en"` + } `json:"keyword_list"` + } `json:"organization_list"` +} + +// Struct that defines the json format for +// url: "https://disco.eduvpn.org/v2/server_list.json" +type servers struct { + v string `json:"v"` + ServerList []struct { + BaseUrl string `json:"base_url"` + CountryCode string `json:"country_code"` + PublicKeyList []string `json:"public_key_list"` + ServerType string `json:"secure_internet"` + SupportContact []string `json:"support_contact"` + } `json:"server_list"` +} + +// Helper function that gets a disco json +// TODO: Verify signature +func getDiscoJson(jsonFile string, structure interface{}) bool { + url := "https://disco.eduvpn.org/v2/" + jsonFile + // Do a Get request to the specified url + resp, reqErr := http.Get(url) + if reqErr != nil { + fmt.Println("error making request") + return false + } + + // Read the body + body, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + fmt.Println("error reading body of request") + return false + } + + // Parse the json using the predefined struct + error := json.Unmarshal([]byte(body), &structure) + if error != nil { + fmt.Println("error parsing server json") + return false + } + return true +} + +// Get the organization list +func getOrganizationList() bool { + organizations := organizations{} + return getDiscoJson("organization_list.json", &organizations) +} + +// Get the server list +func getServerList() bool { + servers := servers{} + return getDiscoJson("server_list.json", &servers) +} diff --git a/src/verify.go b/src/verify.go new file mode 100644 index 0000000..336ba73 --- /dev/null +++ b/src/verify.go @@ -0,0 +1,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} +} |
