summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorjwijenbergh <jeroenwijenbergh@protonmail.com>2022-02-08 15:56:53 +0100
committerjwijenbergh <jeroenwijenbergh@protonmail.com>2022-04-05 12:26:10 +0200
commit70b4bad8904fe02fe4d783b75c6137ba959363ec (patch)
treecaa512596ed5accde73acb31b232d5540edfb401 /src
parent32bd89a77c8aa2fed235291171791def04724b8d (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.go76
-rw-r--r--src/verify.go200
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}
+}