diff options
Diffstat (limited to 'src')
46 files changed, 0 insertions, 2260 deletions
diff --git a/src/api.go b/src/api.go deleted file mode 100644 index 93c1c42..0000000 --- a/src/api.go +++ /dev/null @@ -1,110 +0,0 @@ -package eduvpn - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" -) - -// Authenticated wrappers on top of HTTP -func (server *Server) apiAuthenticated(method string, endpoint string, opts *HTTPOptionalParams) (http.Header, []byte, error) { - // Ensure optional is not nil as we will fill it with headers - if opts == nil { - opts = &HTTPOptionalParams{} - } - url := server.Endpoints.API.V3.API + endpoint - - // Ensure we have valid tokens - oauthErr := server.OAuth.EnsureTokens() - - if oauthErr != nil { - return nil, nil, oauthErr - } - - headerKey := "Authorization" - headerValue := fmt.Sprintf("Bearer %s", server.OAuth.Token.Access) - if opts.Headers != nil { - opts.Headers.Add(headerKey, headerValue) - } else { - opts.Headers = http.Header{headerKey: {headerValue}} - } - return HTTPMethodWithOpts(method, url, opts) -} - -func (server *Server) apiAuthenticatedRetry(method string, endpoint string, opts *HTTPOptionalParams) (http.Header, []byte, error) { - header, body, bodyErr := server.apiAuthenticated(method, endpoint, opts) - if bodyErr != nil { - var error *HTTPStatusError - - // Only retry authenticated if we get a HTTP 401 - if errors.As(bodyErr, &error) && error.Status == 401 { - GetVPNState().Log(LOG_INFO, fmt.Sprintf("API: Got HTTP error %v, retrying authenticated", error)) - // Tell the method that the token is expired - server.OAuth.Token.ExpiredTimestamp = GenerateTimeSeconds() - return server.apiAuthenticated(method, endpoint, opts) - } - return header, nil, bodyErr - } - return header, body, bodyErr -} - -func (server *Server) APIInfo() error { - _, body, bodyErr := server.apiAuthenticatedRetry(http.MethodGet, "/info", nil) - if bodyErr != nil { - return bodyErr - } - structure := ServerProfileInfo{} - jsonErr := json.Unmarshal(body, &structure) - - if jsonErr != nil { - return jsonErr - } - - server.Profiles = structure - server.ProfilesRaw = string(body) - return nil -} - -func (server *Server) APIConnectWireguard(profile_id string, pubkey string) (string, string, error) { - headers := http.Header{ - "content-type": {"application/x-www-form-urlencoded"}, - "accept": {"application/x-wireguard-profile"}, - } - - urlForm := url.Values{ - "profile_id": {profile_id}, - "public_key": {pubkey}, - } - header, connectBody, connectErr := server.apiAuthenticatedRetry(http.MethodPost, "/connect", &HTTPOptionalParams{Headers: headers, Body: urlForm}) - if connectErr != nil { - return "", "", connectErr - } - - expires := header.Get("expires") - return string(connectBody), expires, nil -} - -func (server *Server) APIConnectOpenVPN(profile_id string) (string, string, error) { - headers := http.Header{ - "content-type": {"application/x-www-form-urlencoded"}, - "accept": {"application/x-openvpn-profile"}, - } - - urlForm := url.Values{ - "profile_id": {profile_id}, - } - header, connectBody, connectErr := server.apiAuthenticatedRetry(http.MethodPost, "/connect", &HTTPOptionalParams{Headers: headers, Body: urlForm}) - if connectErr != nil { - return "", "", connectErr - } - - expires := header.Get("expires") - return string(connectBody), expires, nil -} - -// This needs no further return value as it's best effort -func (server *Server) APIDisconnect() { - server.apiAuthenticatedRetry(http.MethodPost, "/disconnect", nil) -} diff --git a/src/config.go b/src/config.go deleted file mode 100644 index 6db3cb2..0000000 --- a/src/config.go +++ /dev/null @@ -1,42 +0,0 @@ -package eduvpn - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path" -) - -func (eduvpn *VPNState) EnsureConfigDir() error { - mkdirErr := os.MkdirAll(eduvpn.ConfigDirectory, os.ModePerm) - if mkdirErr != nil { - return mkdirErr - } - return nil -} - -func (eduvpn *VPNState) GetConfigName() string { - pathString := path.Join(eduvpn.ConfigDirectory, eduvpn.Name) - return fmt.Sprintf("%s.json", pathString) -} - -func (eduvpn *VPNState) WriteConfig() error { - configDirErr := eduvpn.EnsureConfigDir() - if configDirErr != nil { - return configDirErr - } - jsonString, marshalErr := json.Marshal(eduvpn) - if marshalErr != nil { - return marshalErr - } - return ioutil.WriteFile(eduvpn.GetConfigName(), jsonString, 0o644) -} - -func (eduvpn *VPNState) LoadConfig() error { - bytes, readErr := ioutil.ReadFile(eduvpn.GetConfigName()) - if readErr != nil { - return readErr - } - return json.Unmarshal(bytes, eduvpn) -} diff --git a/src/discovery.go b/src/discovery.go deleted file mode 100644 index fa3cac6..0000000 --- a/src/discovery.go +++ /dev/null @@ -1,165 +0,0 @@ -package eduvpn - -import ( - "encoding/json" - "fmt" -) - -type DiscoFileError struct { - URL string - Err error -} - -func (e *DiscoFileError) Error() string { - return fmt.Sprintf("failed obtaining disco file %s with error %v", e.URL, e.Err) -} - -type DiscoSigFileError struct { - URL string - Err error -} - -func (e *DiscoSigFileError) Error() string { - return fmt.Sprintf("failed obtaining disco signature file %s with error %v", e.URL, e.Err) -} - -type DiscoVerifyError struct { - File string - Sigfile string - Err error -} - -func (e *DiscoVerifyError) Error() string { - return fmt.Sprintf("failed verifying file %s with signature %s due to error %v", e.File, e.Sigfile, e.Err) -} - -type DiscoJSONError struct { - Body string - Err error -} - -func (e *DiscoJSONError) Error() string { - return fmt.Sprintf("failed parsing JSON for contents %s with error %v", e.Body, e.Err) -} - -type OrganizationList struct { - JSON json.RawMessage `json:"organization_list"` - Version uint64 `json:"v"` - Timestamp int64 `json:"-"` -} - -type ServersList struct { - JSON json.RawMessage `json:"server_list"` - Version uint64 `json:"v"` - Timestamp int64 `json:"-"` -} - -type DiscoLists struct { - Organizations OrganizationList - Servers ServersList -} - -// Helper function that gets a disco json -func getDiscoFile(jsonFile string, previousVersion uint64, structure interface{}) error { - // Get json data - discoURL := "https://disco.eduvpn.org/v2/" - fileURL := discoURL + jsonFile - _, fileBody, fileErr := HTTPGet(fileURL) - - if fileErr != nil { - return &DiscoFileError{fileURL, fileErr} - } - - // Get signature - sigFile := jsonFile + ".minisig" - sigURL := discoURL + sigFile - _, sigBody, sigFileErr := HTTPGet(sigURL) - - if sigFileErr != nil { - return &DiscoSigFileError{URL: sigURL, Err: sigFileErr} - } - - // Verify signature - // Set this to true when we want to force prehash - forcePrehash := false - verifySuccess, verifyErr := Verify(string(sigBody), fileBody, jsonFile, previousVersion, forcePrehash) - - if !verifySuccess || verifyErr != nil { - return &DiscoVerifyError{File: jsonFile, Sigfile: sigFile, Err: verifyErr} - } - - // Parse JSON to extract version and list - jsonErr := json.Unmarshal(fileBody, structure) - - if jsonErr != nil { - return &DiscoJSONError{Body: string(fileBody), Err: jsonErr} - } - - return nil -} - -type GetListError struct { - File string - Err error -} - -func (e *GetListError) Error() string { - return fmt.Sprintf("failed getting disco list file %s with error %v", e.File, e.Err) -} - -// FIXME: Implement based on -// https://github.com/eduvpn/documentation/blob/v3/SERVER_DISCOVERY.md -// - [IMPLEMENTED] on "first launch" when offering the search for "Institute Access" and "Organizations"; -// - [TODO] when the user tries to add new server AND the user did NOT yet choose an organization before; -// - [TODO] when the authorization for the server associated with an already chosen organization is triggered, e.g. after expiry or revocation. -func (eduvpn *VPNState) DetermineOrganizationsUpdate() bool { - return string(eduvpn.DiscoList.Organizations.JSON) == "" -} - -// https://github.com/eduvpn/documentation/blob/v3/SERVER_DISCOVERY.md -// - [Implemented] The application MUST always fetch the server_list.json at application start. -// - The application MAY refresh the server_list.json periodically, e.g. once every hour. -func (eduvpn *VPNState) DetermineServersUpdate() bool { - // No servers, we should update - if string(eduvpn.DiscoList.Servers.JSON) == "" { - return true - } - // 1 hour from the last update - should_update_time := eduvpn.DiscoList.Servers.Timestamp + 3600 - now := GenerateTimeSeconds() - if now >= should_update_time { - return true - } - GetVPNState().Log(LOG_INFO, "No update needed for servers, 1h is not passed yet") - return false -} - -// Get the organization list -func (eduvpn *VPNState) GetOrganizationsList() (string, error) { - if !eduvpn.DetermineOrganizationsUpdate() { - return string(eduvpn.DiscoList.Organizations.JSON), nil - } - file := "organization_list.json" - err := getDiscoFile(file, eduvpn.DiscoList.Organizations.Version, &eduvpn.DiscoList.Organizations) - if err != nil { - // Return previous with an error - return string(eduvpn.DiscoList.Organizations.JSON), &GetListError{File: file, Err: err} - } - return string(eduvpn.DiscoList.Organizations.JSON), nil -} - -// Get the server list -func (eduvpn *VPNState) GetServersList() (string, error) { - if !eduvpn.DetermineServersUpdate() { - return string(eduvpn.DiscoList.Servers.JSON), nil - } - file := "server_list.json" - err := getDiscoFile(file, eduvpn.DiscoList.Servers.Version, &eduvpn.DiscoList.Servers) - if err != nil { - // Return previous with an error - return string(eduvpn.DiscoList.Servers.JSON), &GetListError{File: file, Err: err} - } - // Update servers timestamp - eduvpn.DiscoList.Servers.Timestamp = GenerateTimeSeconds() - return string(eduvpn.DiscoList.Servers.JSON), nil -} diff --git a/src/fsm.go b/src/fsm.go deleted file mode 100644 index 223b42f..0000000 --- a/src/fsm.go +++ /dev/null @@ -1,197 +0,0 @@ -package eduvpn - -import ( - "fmt" - "os" - "sort" -) - -type ( - FSMStateID int8 - FSMStateIDSlice []FSMStateID -) - -func (v FSMStateIDSlice) Len() int { - return len(v) -} - -func (v FSMStateIDSlice) Less(i, j int) bool { - return v[i] < v[j] -} - -func (v FSMStateIDSlice) Swap(i, j int) { - v[i], v[j] = v[j], v[i] -} - -const ( - // Deregistered means the app is not registered with the wrapper - DEREGISTERED FSMStateID = iota - - // No Server means the user has not chosen a server yet - NO_SERVER - - // Chosen Server means the user has chosen a server to connect to - CHOSEN_SERVER - - // OAuth Started means the OAuth process has started - OAUTH_STARTED - - // Authenticated means the OAuth process has finished and the user is now authenticated with the server - AUTHENTICATED - - // Requested config means the user has requested a config for connecting - REQUEST_CONFIG - - // Has config means the user has gotten a config - HAS_CONFIG - - // Ask profile means the go code is asking for a profile selection from the ui - ASK_PROFILE - - // Connected means the user has been connected to the server - CONNECTED -) - -func (s FSMStateID) String() string { - switch s { - case DEREGISTERED: - return "Deregistered" - case NO_SERVER: - return "No_Server" - case CHOSEN_SERVER: - return "Chosen_Server" - case OAUTH_STARTED: - return "OAuth_Started" - case HAS_CONFIG: - return "Has_Config" - case REQUEST_CONFIG: - return "Request_Config" - case ASK_PROFILE: - return "Ask_Profile" - case AUTHENTICATED: - return "Authenticated" - case CONNECTED: - return "Connected" - default: - panic("unknown conversion of state to string") - } -} - -type FSMTransition struct { - To FSMStateID - Description string -} - -type ( - FSMTransitions []FSMTransition - FSMStates map[FSMStateID]FSMTransitions -) - -type FSM struct { - States FSMStates - Current FSMStateID -} - -func (eduvpn *VPNState) HasTransition(check FSMStateID) bool { - for _, transition_state := range eduvpn.FSM.States[eduvpn.FSM.Current] { - if transition_state.To == check { - return true - } - } - - return false -} - -func (eduvpn *VPNState) InState(check FSMStateID) bool { - return check == eduvpn.FSM.Current -} - -func (eduvpn *VPNState) writeGraph() { - graph := eduvpn.GenerateGraph() - - f, err := os.Create("debug.graph") - if err != nil { - eduvpn.Log(LOG_INFO, fmt.Sprintf("Failed to write debug fsm graph with error %v", err)) - } - - defer f.Close() - - f.WriteString(graph) -} - -func (eduvpn *VPNState) GoTransitionWithData(newState FSMStateID, data string) bool { - ok := eduvpn.HasTransition(newState) - - if ok { - oldState := eduvpn.FSM.Current - eduvpn.FSM.Current = newState - if eduvpn.Debug { - eduvpn.writeGraph() - } - eduvpn.StateCallback(oldState.String(), newState.String(), data) - } - - return ok -} - -func (eduvpn *VPNState) GoTransition(newState FSMStateID) bool { - return eduvpn.GoTransitionWithData(newState, "") -} - -func (eduvpn *VPNState) generateDotGraph() string { - graph := `digraph eduvpn_fsm { -nodesep = 2; -remincross = false; -` - graph += "node[color=blue]; " + eduvpn.FSM.Current.String() + ";\n" - graph += "node [color=black];\n" - for state, transitions := range eduvpn.FSM.States { - for _, transition := range transitions { - graph += state.String() + " -> " + transition.To.String() + " [label=\"" + transition.Description + "\"]\n" - } - } - graph += "}" - return graph -} - -func (eduvpn *VPNState) generateMermaidGraph() string { - graph := "graph TD\n" - sorted_fsm := make(FSMStateIDSlice, 0, len(eduvpn.FSM.States)) - for state_id := range eduvpn.FSM.States { - sorted_fsm = append(sorted_fsm, state_id) - } - sort.Sort(sorted_fsm) - for _, state := range sorted_fsm { - transitions := eduvpn.FSM.States[state] - for _, transition := range transitions { - if state == eduvpn.FSM.Current { - graph += "\nstyle " + state.String() + " fill:cyan\n" - } else { - graph += "\nstyle " + state.String() + " fill:white\n" - } - graph += state.String() + "(" + state.String() + ") " + "-->|" + transition.Description + "| " + transition.To.String() + "\n" - } - } - return graph -} - -func (eduvpn *VPNState) GenerateGraph() string { - return eduvpn.generateMermaidGraph() -} - -func (eduvpn *VPNState) InitializeFSM() { - eduvpn.FSM = FSM{ - States: FSMStates{ - DEREGISTERED: {{NO_SERVER, "Client registers"}}, - NO_SERVER: {{CHOSEN_SERVER, "User chooses a server"}}, - CHOSEN_SERVER: {{AUTHENTICATED, "Found tokens in config"}, {OAUTH_STARTED, "No tokens found in config"}}, - OAUTH_STARTED: {{AUTHENTICATED, "User authorizes with browser"}}, - AUTHENTICATED: {{OAUTH_STARTED, "Re-authenticate with OAuth"}, {REQUEST_CONFIG, "Client requests a config"}}, - REQUEST_CONFIG: {{ASK_PROFILE, "Multiple profiles found"}, {HAS_CONFIG, "Success, only one profile"}}, - ASK_PROFILE: {{HAS_CONFIG, "User chooses profile and success"}}, - HAS_CONFIG: {{CONNECTED, "OS reports connected"}}, - CONNECTED: {{AUTHENTICATED, "OS reports disconnected"}}, - }, - Current: DEREGISTERED, - } -} diff --git a/src/http.go b/src/http.go deleted file mode 100644 index bbc866b..0000000 --- a/src/http.go +++ /dev/null @@ -1,172 +0,0 @@ -package eduvpn - -import ( - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "strings" -) - -type HTTPResourceError struct { - URL string - Err error -} - -func (e *HTTPResourceError) Error() string { - return fmt.Sprintf("failed obtaining HTTP resource %s with error %v", e.URL, e.Err) -} - -type HTTPStatusError struct { - URL string - Status int -} - -func (e *HTTPStatusError) Error() string { - return fmt.Sprintf("failed obtaining HTTP resource %s as it gave an unsuccesful status code %d", e.URL, e.Status) -} - -type HTTPReadError struct { - URL string - Err error -} - -func (e *HTTPReadError) Error() string { - return fmt.Sprintf("failed reading HTTP resource %s with error %v", e.URL, e.Err) -} - -type HTTPParseJsonError struct { - URL string - Body string - Err error -} - -func (e *HTTPParseJsonError) Error() string { - return fmt.Sprintf("failed parsing json %s for HTTP resource %s with error %v", e.Body, e.URL, e.Err) -} - -type HTTPRequestCreateError struct { - URL string - Err error -} - -func (e *HTTPRequestCreateError) Error() string { - return fmt.Sprintf("failed to create HTTP request with url %s and error %v", e.URL, e.Err) -} - -type URLParameters map[string]string - -type HTTPOptionalParams struct { - Headers http.Header - URLParameters URLParameters - Body url.Values -} - -// Construct an URL including on parameters -func HTTPConstructURL(baseURL string, parameters URLParameters) (string, error) { - url, err := url.Parse(baseURL) - if err != nil { - return "", err - } - - q := url.Query() - - for parameter, value := range parameters { - q.Set(parameter, value) - } - url.RawQuery = q.Encode() - return url.String(), nil -} - -// Convenience functions -func HTTPGet(url string) (http.Header, []byte, error) { - return HTTPMethodWithOpts(http.MethodGet, url, nil) -} - -func HTTPPost(url string, body url.Values) (http.Header, []byte, error) { - return HTTPMethodWithOpts(http.MethodGet, url, &HTTPOptionalParams{Body: body}) -} - -func HTTPGetWithOpts(url string, opts *HTTPOptionalParams) (http.Header, []byte, error) { - return HTTPMethodWithOpts(http.MethodGet, url, opts) -} - -func HTTPPostWithOpts(url string, opts *HTTPOptionalParams) (http.Header, []byte, error) { - return HTTPMethodWithOpts(http.MethodPost, url, opts) -} - -func httpOptionalURL(url string, opts *HTTPOptionalParams) (string, error) { - if opts != nil { - url, urlErr := HTTPConstructURL(url, opts.URLParameters) - - if urlErr != nil { - return url, &HTTPRequestCreateError{URL: url, Err: urlErr} - } - return url, nil - } - return url, nil -} - -func httpOptionalHeaders(req *http.Request, opts *HTTPOptionalParams) { - // Add headers - if opts != nil && req != nil { - for k, v := range opts.Headers { - req.Header.Add(k, v[0]) - } - } -} - -func httpOptionalBodyReader(opts *HTTPOptionalParams) io.Reader { - if opts != nil && opts.Body != nil { - return strings.NewReader(opts.Body.Encode()) - } - return nil -} - -func HTTPMethodWithOpts(method string, url string, opts *HTTPOptionalParams) (http.Header, []byte, error) { - // Make sure the url contains all the parameters - // This can return an error, - // it already has the right error so so we don't wrap it further - url, urlErr := httpOptionalURL(url, opts) - if urlErr != nil { - return nil, nil, urlErr - } - - // Create a client - client := &http.Client{} - - // Create request object with the body reader generated from the optional arguments - req, reqErr := http.NewRequest(method, url, httpOptionalBodyReader(opts)) - if reqErr != nil { - return nil, nil, &HTTPRequestCreateError{URL: url, Err: reqErr} - } - - // See https://stackoverflow.com/questions/17714494/golang-http-request-results-in-eof-errors-when-making-multiple-requests-successi - req.Close = true - - // Make sure the headers contain all the parameters - httpOptionalHeaders(req, opts) - - // Do request - resp, respErr := client.Do(req) - if respErr != nil { - return nil, nil, &HTTPResourceError{URL: url, Err: respErr} - } - - // Request successful, make sure body is closed at the end - defer resp.Body.Close() - - // Return a string - body, readErr := ioutil.ReadAll(resp.Body) - if readErr != nil { - return resp.Header, nil, &HTTPReadError{URL: url, Err: readErr} - } - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return resp.Header, body, &HTTPStatusError{URL: url, Status: resp.StatusCode} - } - - // Return the body in bytes and signal the status error if there was one - return resp.Header, body, nil -} diff --git a/src/log.go b/src/log.go deleted file mode 100644 index 7402e31..0000000 --- a/src/log.go +++ /dev/null @@ -1,66 +0,0 @@ -package eduvpn - -import ( - "fmt" - "log" - "os" - "path" -) - -type FileLogger struct { - Level LogLevel - File *os.File -} - -type LogLevel int8 - -const ( - LOG_NOTSET LogLevel = iota - LOG_INFO - LOG_WARNING - LOG_ERROR -) - -func (e LogLevel) String() string { - switch e { - case LOG_NOTSET: - return "NOTSET" - case LOG_INFO: - return "INFO" - case LOG_WARNING: - return "WARNING" - case LOG_ERROR: - return "ERROR" - default: - return "UNKNOWN" - } -} - -func (eduvpn *VPNState) getLogFilename() string { - pathString := path.Join(eduvpn.ConfigDirectory, eduvpn.Name) - return fmt.Sprintf("%s.log", pathString) -} - -func (eduvpn *VPNState) InitLog(level LogLevel) error { - configDirErr := eduvpn.EnsureConfigDir() - if configDirErr != nil { - return configDirErr - } - logFile, logOpenErr := os.OpenFile(eduvpn.getLogFilename(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) - if logOpenErr != nil { - return logOpenErr - } - log.SetOutput(logFile) - eduvpn.LogFile = FileLogger{Level: level, File: logFile} - return nil -} - -func (eduvpn *VPNState) Log(level LogLevel, str string) { - if level >= eduvpn.LogFile.Level && eduvpn.LogFile.Level != LOG_NOTSET { - log.Printf("[%s]: %s", level.String(), str) - } -} - -func (eduvpn *VPNState) CloseLog() { - eduvpn.LogFile.File.Close() -} diff --git a/src/oauth.go b/src/oauth.go deleted file mode 100644 index 263690e..0000000 --- a/src/oauth.go +++ /dev/null @@ -1,393 +0,0 @@ -package eduvpn - -import ( - "context" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" -) - -// Generates a random base64 string to be used for state -// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-4.1.1 -// "state": OPTIONAL. An opaque value used by the client to maintain -// state between the request and callback. The authorization server -// includes this value when redirecting the user agent back to the -// client. -func genState() (string, error) { - randomBytes, err := MakeRandomByteSlice(32) - if err != nil { - return "", &OAuthGenStateUnableError{Err: err} - } - - // For consistency we also use raw url encoding here - return base64.RawURLEncoding.EncodeToString(randomBytes), nil -} - -// Generates a sha256 base64 challenge from a verifier -// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-7.8 -func genChallengeS256(verifier string) string { - hash := sha256.Sum256([]byte(verifier)) - - // We use raw url encoding as the challenge does not accept padding - return base64.RawURLEncoding.EncodeToString(hash[:]) -} - -// Generates a verifier -// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-4.1.1 -// The code_verifier is a unique high-entropy cryptographically random -// string generated for each authorization request, using the unreserved -// characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a -// minimum length of 43 characters and a maximum length of 128 -// characters. -func genVerifier() (string, error) { - randomBytes, err := MakeRandomByteSlice(32) - if err != nil { - return "", &OAuthGenVerifierUnableError{Err: err} - } - - return base64.RawURLEncoding.EncodeToString(randomBytes), nil -} - -type OAuth struct { - Session OAuthExchangeSession `json:"-"` - Token OAuthToken `json:"token"` - TokenURL string `json:"token_url"` -} - -// This structure gets passed to the callback for easy access to the current state -type OAuthExchangeSession struct { - // returned from the callback - CallbackError error - - // filled in in initialize - ClientID string - State string - Verifier string - - // filled in when constructing the callback - Context context.Context - Server http.Server -} - -// Struct that defines the json format for /.well-known/vpn-user-portal" -type OAuthToken struct { - Access string `json:"access_token"` - Refresh string `json:"refresh_token"` - Type string `json:"token_type"` - Expires int64 `json:"expires_in"` - ExpiredTimestamp int64 `json:"expires_in_timestamp"` -} - -// Gets an authenticated HTTP client by obtaining refresh and access tokens -func (oauth *OAuth) getTokensWithCallback() error { - oauth.Session.Context = context.Background() - mux := http.NewServeMux() - addr := "127.0.0.1:8000" - oauth.Session.Server = http.Server{ - Addr: addr, - Handler: mux, - } - mux.HandleFunc("/callback", oauth.Callback) - if err := oauth.Session.Server.ListenAndServe(); err != http.ErrServerClosed { - return &OAuthFailedCallbackError{Addr: addr, Err: err} - } - return oauth.Session.CallbackError -} - -// Get the access and refresh tokens -// Access tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.4 -// Refresh tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.3.2 -func (oauth *OAuth) getTokensWithAuthCode(authCode string) error { - // Make sure the verifier is set as the parameter - // so that the server can verify that we are the actual owner of the authorization code - - reqURL := oauth.TokenURL - data := url.Values{ - "client_id": {oauth.Session.ClientID}, - "code": {authCode}, - "code_verifier": {oauth.Session.Verifier}, - "grant_type": {"authorization_code"}, - "redirect_uri": {"http://127.0.0.1:8000/callback"}, - } - headers := http.Header{ - "content-type": {"application/x-www-form-urlencoded"}, - } - opts := &HTTPOptionalParams{Headers: headers, Body: data} - current_time := GenerateTimeSeconds() - _, body, bodyErr := HTTPPostWithOpts(reqURL, opts) - if bodyErr != nil { - return bodyErr - } - - tokenStructure := OAuthToken{} - - jsonErr := json.Unmarshal(body, &tokenStructure) - - if jsonErr != nil { - return &HTTPParseJsonError{URL: reqURL, Body: string(body), Err: jsonErr} - } - - tokenStructure.ExpiredTimestamp = current_time + tokenStructure.Expires - oauth.Token = tokenStructure - return nil -} - -func (oauth *OAuth) isTokensExpired() bool { - expired_time := oauth.Token.ExpiredTimestamp - current_time := GenerateTimeSeconds() - return current_time >= expired_time -} - -// Get the access and refresh tokens with a previously received refresh token -// Access tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.4 -// Refresh tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.3.2 -func (oauth *OAuth) getTokensWithRefresh() error { - reqURL := oauth.TokenURL - data := url.Values{ - "refresh_token": {oauth.Token.Refresh}, - "grant_type": {"refresh_token"}, - } - headers := http.Header{ - "content-type": {"application/x-www-form-urlencoded"}, - } - opts := &HTTPOptionalParams{Headers: headers, Body: data} - current_time := GenerateTimeSeconds() - _, body, bodyErr := HTTPPostWithOpts(reqURL, opts) - if bodyErr != nil { - return bodyErr - } - - tokenStructure := OAuthToken{} - jsonErr := json.Unmarshal(body, &tokenStructure) - - if jsonErr != nil { - return &HTTPParseJsonError{URL: reqURL, Body: string(body), Err: jsonErr} - } - - tokenStructure.ExpiredTimestamp = current_time + tokenStructure.Expires - oauth.Token = tokenStructure - return nil -} - -// -//// The callback to retrieve the authorization code: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.3.1 -func (oauth *OAuth) Callback(w http.ResponseWriter, req *http.Request) { - // Extract the authorization code - code, success := req.URL.Query()["code"] - if !success { - oauth.Session.CallbackError = &OAuthFailedCallbackParameterError{Parameter: "code", URL: req.URL.String()} - go oauth.Session.Server.Shutdown(oauth.Session.Context) - return - } - // The code is the first entry - extractedCode := code[0] - - // Make sure the state is present and matches to protect against cross-site request forgeries - // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-7.15 - state, success := req.URL.Query()["state"] - if !success { - oauth.Session.CallbackError = &OAuthFailedCallbackParameterError{Parameter: "state", URL: req.URL.String()} - go oauth.Session.Server.Shutdown(oauth.Session.Context) - return - } - // The state is the first entry - extractedState := state[0] - if extractedState != oauth.Session.State { - oauth.Session.CallbackError = &OAuthFailedCallbackStateMatchError{State: extractedState, ExpectedState: oauth.Session.State} - go oauth.Session.Server.Shutdown(oauth.Session.Context) - return - } - - // Now that we have obtained the authorization code, we can move to the next step: - // Obtaining the access and refresh tokens - err := oauth.getTokensWithAuthCode(extractedCode) - if err != nil { - oauth.Session.CallbackError = &OAuthFailedCallbackGetTokensError{Err: err} - go oauth.Session.Server.Shutdown(oauth.Session.Context) - return - } - - // Shutdown the server as we're done listening - go oauth.Session.Server.Shutdown(oauth.Session.Context) -} - -// Initializes the OAuth for eduvpn. -// It needs a vpn state that was gotten from `Register` -// It returns the authurl for the browser and an error if present -func (eduvpn *VPNState) InitializeOAuth() error { - if !eduvpn.HasTransition(OAUTH_STARTED) { - return errors.New(fmt.Sprintf("Failed starting oauth, invalid state %s", eduvpn.FSM.Current.String())) - } - // Generate the state - state, stateErr := genState() - if stateErr != nil { - return &OAuthFailedInitializeError{Err: stateErr} - } - - // Generate the verifier and challenge - verifier, verifierErr := genVerifier() - if verifierErr != nil { - return &OAuthFailedInitializeError{Err: verifierErr} - } - challenge := genChallengeS256(verifier) - - parameters := map[string]string{ - "client_id": eduvpn.Name, - "code_challenge_method": "S256", - "code_challenge": challenge, - "response_type": "code", - "scope": "config", - "state": state, - "redirect_uri": "http://127.0.0.1:8000/callback", - } - - server, serverErr := eduvpn.Servers.GetCurrentServer() - if serverErr != nil { - return errors.New("OAuth Initialize no server found") - } - authURL, urlErr := HTTPConstructURL(server.Endpoints.API.V3.Authorization, parameters) - - if urlErr != nil { // shouldn't happen - panic(urlErr) - } - - // Fill the struct with the necessary fields filled for the next call to getting the HTTP client - oauthSession := OAuthExchangeSession{ClientID: eduvpn.Name, State: state, Verifier: verifier} - server.OAuth = OAuth{TokenURL: server.Endpoints.API.V3.Token, Session: oauthSession} - eduvpn.GoTransitionWithData(OAUTH_STARTED, authURL) - return nil -} - -// Error definitions -func (eduvpn *VPNState) FinishOAuth() error { - if !eduvpn.HasTransition(AUTHENTICATED) { - return errors.New("invalid state to finish oauth") - } - server, serverErr := eduvpn.Servers.GetCurrentServer() - if serverErr != nil { - return errors.New("OAuth Initialize No server found") - } - tokenErr := server.OAuth.getTokensWithCallback() - if tokenErr != nil { - return tokenErr - } - eduvpn.GoTransition(AUTHENTICATED) - return nil -} - -func (state *VPNState) LoginOAuth() error { - authInitializeErr := state.InitializeOAuth() - - if authInitializeErr != nil { - return authInitializeErr - } - - oauthErr := state.FinishOAuth() - - if oauthErr != nil { - return oauthErr - } - return nil -} - -func (oauth *OAuth) Login() error { - return GetVPNState().LoginOAuth() -} - -func (oauth *OAuth) NeedsRelogin() bool { - // Access Token or Refresh Tokens empty, definitely needs a relogin - if oauth.Token.Access == "" || oauth.Token.Refresh == "" { - GetVPNState().Log(LOG_INFO, "OAuth: Tokens are empty") - return true - } - - // We have tokens... - - // The tokens are not expired yet - // No relogin is needed - if !oauth.isTokensExpired() { - GetVPNState().Log(LOG_INFO, "OAuth: Tokens are not expired, re-login not needed") - return false - } - - refreshErr := oauth.getTokensWithRefresh() - // We have obtained new tokens with refresh - if refreshErr == nil { - GetVPNState().Log(LOG_INFO, "OAuth: Tokens could be re-acquired using the refresh token, re-login not needed") - return false - } - - // Otherwise relogin is really needed - return true -} - -func (oauth *OAuth) EnsureTokens() error { - if oauth.NeedsRelogin() { - GetVPNState().Log(LOG_INFO, "OAuth: Tokens are invalid, relogging in") - return oauth.Login() - } - return nil -} - -type OAuthGenStateUnableError struct { - Err error -} - -func (e *OAuthGenStateUnableError) Error() string { - return fmt.Sprintf("failed generating state with error %v", e.Err) -} - -type OAuthGenVerifierUnableError struct { - Err error -} - -func (e *OAuthGenVerifierUnableError) Error() string { - return fmt.Sprintf("failed generating verifier with error %v", e.Err) -} - -type OAuthFailedCallbackError struct { - Addr string - Err error -} - -func (e *OAuthFailedCallbackError) Error() string { - return fmt.Sprintf("failed callback %s with error %v", e.Addr, e.Err) -} - -type OAuthFailedCallbackParameterError struct { - Parameter string - URL string -} - -func (e *OAuthFailedCallbackParameterError) Error() string { - return fmt.Sprintf("failed retrieving parameter %s in url %s", e.Parameter, e.URL) -} - -type OAuthFailedCallbackStateMatchError struct { - State string - ExpectedState string -} - -func (e *OAuthFailedCallbackStateMatchError) Error() string { - return fmt.Sprintf("failed matching state, got %s, want %s", e.State, e.ExpectedState) -} - -type OAuthFailedCallbackGetTokensError struct { - Err error -} - -func (e *OAuthFailedCallbackGetTokensError) Error() string { - return fmt.Sprintf("failed getting tokens with error %v", e.Err) -} - -type OAuthFailedInitializeError struct { - Err error -} - -func (e *OAuthFailedInitializeError) Error() string { - return fmt.Sprintf("failed initializing OAuth with error %v", e.Err) -} diff --git a/src/openvpn.go b/src/openvpn.go deleted file mode 100644 index 95e1328..0000000 --- a/src/openvpn.go +++ /dev/null @@ -1,12 +0,0 @@ -package eduvpn - -func (server *Server) OpenVPNGetConfig() (string, error) { - profile_id := server.Profiles.Current - configOpenVPN, _, configErr := server.APIConnectOpenVPN(profile_id) - - if configErr != nil { - return "", configErr - } - - return configOpenVPN, nil -} diff --git a/src/server.go b/src/server.go deleted file mode 100644 index 3dca26b..0000000 --- a/src/server.go +++ /dev/null @@ -1,178 +0,0 @@ -package eduvpn - -import ( - "encoding/json" - "errors" -) - -type Server struct { - BaseURL string `json:"base_url"` - Endpoints ServerEndpoints `json:"endpoints"` - OAuth OAuth `json:"oauth"` - Profiles ServerProfileInfo `json:"profiles"` - ProfilesRaw string `json:"profiles_raw"` -} - -type Servers struct { - List map[string]*Server `json:"list"` - Current string `json:"current"` -} - -func (servers *Servers) GetCurrentServer() (*Server, error) { - if servers.List == nil { - return nil, errors.New("No map found to get Current Server") - } - server, exists := servers.List[servers.Current] - - if !exists || server == nil { - return nil, errors.New("Current Server not found") - } - return server, nil -} - -func (servers *Servers) EnsureServer(url string) *Server { - if servers.List == nil { - servers.List = make(map[string]*Server) - } - - server, exists := servers.List[url] - - if !exists || server == nil { - server = &Server{} - server.Initialize(url) - servers.List[url] = server - } - servers.Current = url - return server -} - -type ServerProfile struct { - ID string `json:"profile_id"` - DisplayName string `json:"display_name"` - VPNProtoList []string `json:"vpn_proto_list"` - DefaultGateway bool `json:"default_gateway"` -} - -type ServerProfileInfo struct { - Current string `json:"current_profile"` - Info struct { - ProfileList []ServerProfile `json:"profile_list"` - } `json:"info"` -} - -type ServerEndpointList struct { - API string `json:"api_endpoint"` - Authorization string `json:"authorization_endpoint"` - Token string `json:"token_endpoint"` -} - -// Struct that defines the json format for /.well-known/vpn-user-portal" -type ServerEndpoints struct { - API struct { - V2 ServerEndpointList `json:"http://eduvpn.org/api#2"` - V3 ServerEndpointList `json:"http://eduvpn.org/api#3"` - } `json:"api"` - V string `json:"v"` -} - -func (server *Server) Initialize(url string) error { - server.BaseURL = url - endpointsErr := server.GetEndpoints() - if endpointsErr != nil { - return endpointsErr - } - return nil -} - -func (server *Server) NeedsRelogin() bool { - // Check if OAuth needs relogin - return server.OAuth.NeedsRelogin() -} - -func (server *Server) GetEndpoints() error { - url := server.BaseURL + "/.well-known/vpn-user-portal" - _, body, bodyErr := HTTPGet(url) - - if bodyErr != nil { - return bodyErr - } - - endpoints := ServerEndpoints{} - jsonErr := json.Unmarshal(body, &endpoints) - - if jsonErr != nil { - return jsonErr - } - - server.Endpoints = endpoints - - return nil -} - -func (profile *ServerProfile) supportsWireguard() bool { - for _, proto := range profile.VPNProtoList { - if proto == "wireguard" { - return true - } - } - return false -} - -func (server *Server) getCurrentProfile() (*ServerProfile, error) { - profile_id := server.Profiles.Current - for _, profile := range server.Profiles.Info.ProfileList { - if profile.ID == profile_id { - return &profile, nil - } - } - return nil, errors.New("no profile found for id") -} - -func (server *Server) getConfigWithProfile() (string, error) { - if !GetVPNState().HasTransition(HAS_CONFIG) { - return "", errors.New("cannot get a config with a profile, invalid state") - } - profile, profileErr := server.getCurrentProfile() - - if profileErr != nil { - return "", profileErr - } - - if profile.supportsWireguard() { - return server.WireguardGetConfig() - } - return server.OpenVPNGetConfig() -} - -func (server *Server) askForProfileID() error { - if !GetVPNState().HasTransition(ASK_PROFILE) { - return errors.New("cannot ask for a profile id, invalid state") - } - GetVPNState().GoTransitionWithData(ASK_PROFILE, server.ProfilesRaw) - return nil -} - -func (server *Server) GetConfig() (string, error) { - if !GetVPNState().InState(REQUEST_CONFIG) { - return "", errors.New("cannot get a config, invalid state") - } - infoErr := server.APIInfo() - - if infoErr != nil { - return "", infoErr - } - - // Set the current profile if there is only one profile - if len(server.Profiles.Info.ProfileList) == 1 { - server.Profiles.Current = server.Profiles.Info.ProfileList[0].ID - return server.getConfigWithProfile() - } - - profileErr := server.askForProfileID() - - if profileErr != nil { - return "", nil - } - - return server.getConfigWithProfile() -} diff --git a/src/server_test.go b/src/server_test.go deleted file mode 100644 index ccf58f6..0000000 --- a/src/server_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package eduvpn - -import ( - "crypto/tls" - "errors" - "fmt" - "net/http" - "os" - "os/exec" - "strconv" - "strings" - "testing" - "time" -) - -func runCommand(t *testing.T, errBuffer *strings.Builder, name string, args ...string) error { - cmd := exec.Command(name, args...) - - cmd.Stderr = errBuffer - err := cmd.Start() - if err != nil { - return err - } - - return cmd.Wait() -} - -func LoginOAuthSelenium(t *testing.T, url string) { - // We could use the go selenium library - // But it does not support the latest selenium v4 just yet - var errBuffer strings.Builder - err := runCommand(t, &errBuffer, "python3", "../selenium_eduvpn.py", url) - if err != nil { - t.Errorf("Login OAuth with selenium script failed with error %v and stderr %s", err, errBuffer.String()) - } -} - -func StateCallback(t *testing.T, oldState string, newState string, data string) { - if newState == "OAuth_Started" { - go LoginOAuthSelenium(t, data) - } -} - -func Test_server(t *testing.T) { - state := GetVPNState() - - // Do not verify because during testing, the cert is self-signed - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - state.Register("org.eduvpn.app.linux", "configstest", func(old string, new string, data string) { - StateCallback(t, old, new, data) - }, false) - - _, configErr := state.Connect("https://eduvpnserver") - - if configErr != nil { - t.Errorf("Connect error: %v", configErr) - } -} - -func test_connect_oauth_parameter(t *testing.T, parameters URLParameters, expectedErr interface{}) { - state := GetVPNState() - state.Deregister() - - // Do not verify because during testing, the cert is self-signed - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - state.Register("org.eduvpn.app.linux", "configsnologin", func(oldState string, newState string, data string) { - if newState == "OAuth_Started" { - baseURL := "http://127.0.0.1:8000/callback" - url, err := HTTPConstructURL(baseURL, parameters) - if err != nil { - t.Errorf("Error: Constructing url %s with parameters %s", baseURL, fmt.Sprint(parameters)) - } - go http.Get(url) - - } - }, false) - _, configErr := state.Connect("https://eduvpnserver") - - if !errors.As(configErr, expectedErr) { - t.Errorf("error %T = %v, wantErr %T", configErr, configErr, expectedErr) - } -} - -func Test_connect_oauth_parameters(t *testing.T) { - var ( - failedCallbackParameterError *OAuthFailedCallbackParameterError - failedCallbackStateMatchError *OAuthFailedCallbackStateMatchError - ) - - tests := []struct { - expectedErr interface{} - parameters URLParameters - }{ - {&failedCallbackParameterError, URLParameters{}}, - {&failedCallbackParameterError, URLParameters{"code": "42"}}, - {&failedCallbackStateMatchError, URLParameters{"code": "42", "state": "21"}}, - } - - for _, test := range tests { - test_connect_oauth_parameter(t, test.parameters, test.expectedErr) - } -} - -func Test_token_expired(t *testing.T) { - expiredTTL := os.Getenv("OAUTH_EXPIRED_TTL") - if expiredTTL == "" { - t.Log("No expired TTL present, skipping this test. Set EXPIRED_TTL env variable to run it") - return - } - - // Convert the env variable to an int and signal error if it is not possible - expiredInt, expiredErr := strconv.Atoi(expiredTTL) - if expiredErr != nil { - t.Errorf("Cannot convert EXPIRED_TTL env variable to an int with error %v", expiredErr) - } - - // Get a vpn state - state := GetVPNState() - - state.Deregister() - - // Do not verify because during testing, the cert is self-signed - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - state.Register("org.eduvpn.app.linux", "configsexpired", func(old string, new string, data string) { - StateCallback(t, old, new, data) - }, false) - - _, configErr := state.Connect("https://eduvpnserver") - - if configErr != nil { - t.Errorf("Connect error before expired: %v", configErr) - } - - server, serverErr := state.Servers.GetCurrentServer() - if serverErr != nil { - t.Errorf("No server found") - } - - accessToken := server.OAuth.Token.Access - refreshToken := server.OAuth.Token.Refresh - - // Wait for TTL so that the tokens expire - time.Sleep(time.Duration(expiredInt) * time.Second) - - infoErr := server.APIInfo() - - if infoErr != nil { - t.Errorf("Info error after expired: %v", infoErr) - } - - // Check if tokens have changed - accessTokenAfter := server.OAuth.Token.Access - refreshTokenAfter := server.OAuth.Token.Refresh - - if accessToken == accessTokenAfter { - t.Errorf("Access token is the same after refresh") - } - - if refreshToken == refreshTokenAfter { - t.Errorf("Refresh token is the same after refresh") - } -} - -func Test_token_invalid(t *testing.T) { - state := GetVPNState() - - // Do not verify because during testing, the cert is self-signed - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - state.Deregister() - - state.Register("org.eduvpn.app.linux", "configsinvalid", func(old string, new string, data string) { - StateCallback(t, old, new, data) - }, false) - - _, configErr := state.Connect("https://eduvpnserver") - - if configErr != nil { - t.Errorf("Connect error before invalid: %v", configErr) - } - - // Fake connect and then back to authenticated so that we can re-authenticate - // Going to authenticated fakes a disconnect - state.GoTransition(CONNECTED) - state.GoTransition(AUTHENTICATED) - - dummy_value := "37" - - server, serverErr := state.Servers.GetCurrentServer() - if serverErr != nil { - t.Errorf("No server found") - } - - // Override tokens with invalid values - server.OAuth.Token.Access = dummy_value - server.OAuth.Token.Refresh = dummy_value - - infoErr := server.APIInfo() - - if infoErr != nil { - t.Errorf("Info error after invalid: %v", infoErr) - } - - if server.OAuth.Token.Access == dummy_value { - t.Errorf("Access token is equal to dummy value: %s", dummy_value) - } - - if server.OAuth.Token.Refresh == dummy_value { - t.Errorf("Refresh token is equal to dummy value: %s", dummy_value) - } -} diff --git a/src/state.go b/src/state.go deleted file mode 100644 index c0e512f..0000000 --- a/src/state.go +++ /dev/null @@ -1,108 +0,0 @@ -package eduvpn - -import ( - "errors" -) - -type VPNState struct { - // Info passed by the client - ConfigDirectory string `json:"-"` - Name string `json:"-"` - StateCallback func(string, string, string) `json:"-"` - StateCallbackData string `json:"-"` - - // The chosen server - Servers Servers `json:"servers"` - - // The list of servers and organizations from disco - DiscoList DiscoLists `json:"-"` - - // The file we keep open for logging - LogFile FileLogger `json:"-"` - - // The fsm - FSM FSM `json:"-"` - - // Whether to enable debugging - Debug bool `json:"-"` -} - -func (state *VPNState) Register(name string, directory string, stateCallback func(string, string, string), debug bool) error { - if !state.InState(DEREGISTERED) { - return errors.New("app already registered") - } - state.InitializeFSM() - state.Name = name - state.ConfigDirectory = directory - state.StateCallback = stateCallback - state.Debug = debug - - LogLevel := LOG_WARNING - - if debug { - LogLevel = LOG_INFO - } - - // Initialize the logger - state.InitLog(LogLevel) - - // Try to load the previous configuration - if state.LoadConfig() != nil { - // This error can be safely ignored, as when the config does not load, the struct will not be filled - state.Log(LOG_INFO, "Previous configuration not found") - } - state.GoTransition(NO_SERVER) - return nil -} - -func (state *VPNState) Deregister() error { - // Close the log file - state.CloseLog() - - // Write the config - state.WriteConfig() - - // Re-initialize the servers and FSM - state.Servers = Servers{} - state.InitializeFSM() - return nil -} - -func (state *VPNState) Connect(url string) (string, error) { - // New server chosen, ensure the server is fresh - server := state.Servers.EnsureServer(url) - // Make sure we are in the chosen state if available - state.GoTransition(CHOSEN_SERVER) - // Relogin with oauth - // This moves the state to authenticated - if server.NeedsRelogin() { - loginErr := state.LoginOAuth() - - if loginErr != nil { - return "", loginErr - } - } else { // OAuth was valid, ensure we are in the authenticated state - state.GoTransition(AUTHENTICATED) - } - - state.GoTransition(REQUEST_CONFIG) - - config, configErr := server.GetConfig() - - if configErr != nil { - return "", configErr - } else { - state.GoTransition(HAS_CONFIG) - } - - return config, nil -} - -var VPNStateInstance *VPNState - -func GetVPNState() *VPNState { - if VPNStateInstance == nil { - VPNStateInstance = &VPNState{} - } - return VPNStateInstance -} diff --git a/src/test_data/empty b/src/test_data/empty deleted file mode 100644 index e69de29..0000000 --- a/src/test_data/empty +++ /dev/null diff --git a/src/test_data/generate.sh b/src/test_data/generate.sh deleted file mode 100644 index b1b4545..0000000 --- a/src/test_data/generate.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# Generate testcases with fake keys - -# Make sure we do not delete *.minisigs etc. in the wrong directory -if [ ${PWD##*/} != "test_data" ] -then - >&2 echo "Wrong directory, should be run in test_data/" - exit 1 -fi - -rm -f *.minisig *.blake2b - -# Uncomment to regenerate keys -#rm -f *.key -#echo -en "\n\n" | minisign -Gf -p public.key -s secret.key & -#echo -en "\n\n" | minisign -Gf -p wrong_public.key -s wrong_secret.key & -#wait - -# Try to create pure signature with default Minisign (works with version < 0.10) -echo | minisign -Sm server_list.json -x server_list.json.pure.minisig -t $'timestamp:10\tfile:server_list.json' -s secret.key -# Check if it is actually a prehashed signature -if echo | minisign -VHm server_list.json -x server_list.json.pure.minisig -p public.key -then - echo "minisign version is >0.9, trying minisign-0.9" - # If it is, try to sign with some minisign-0.9 program - if ! echo | minisign-0.9 -Sm server_list.json -x server_list.json.pure.minisig -t $'timestamp:10\tfile:server_list.json' -s secret.key - then - >&2 echo -e "\n\nTo produce a non-prehashed signature we need Minisign 0.9\n\n" - fi -fi - -# Rest works with Minisign 0.9 and 0.10 (and up, probably) - -echo | minisign -SHm server_list.json -t $'timestamp:10\tfile:server_list.json\thashed' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.tc_nohashed.minisig -t $'timestamp:10\tfile:server_list.json' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.tc_latertime.minisig -t $'timestamp:20\tfile:server_list.json\t hashed' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.tc_orglist.minisig -t $'timestamp:10\tfile:organization_list.json\thashed' -s secret.key & -wait -echo | minisign -SHm server_list.json -x server_list.json.tc_otherfile.minisig -t $'timestamp:10\tfile:otherfile\thashed' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.tc_nofile.minisig -t $'timestamp:10\thashed' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.tc_notime.minisig -t $'file:server_list.json\thashed' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.tc_emptytime.minisig -t $'timestamp:\tfile:server_list.json\thashed' -s secret.key & -wait -echo | minisign -SHm server_list.json -x server_list.json.tc_emptyfile.minisig -t $'timestamp:10\tfile:\thashed' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.tc_earliertime.minisig -t $'timestamp:9\tfile:server_list.json\thashed' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.tc_random.minisig -t 'random stuff' -s secret.key & -echo | minisign -SHm server_list.json -x server_list.json.large_time.minisig -t $'timestamp:4300000000\tfile:server_list.json' -s secret.key & -wait - -echo | minisign -SHm organization_list.json -t $'timestamp:10\tfile:organization_list.json\thashed' -s secret.key & -echo | minisign -SHm organization_list.json -x organization_list.json.tc_servlist.minisig -t $'timestamp:10\tfile:server_list.json\thashed' -s secret.key & - -echo | minisign -SHm other_list.json -t $'timestamp:10\tfile:other_list.json\thashed' -s secret.key & - -echo | minisign -SHm server_list.json -x server_list.json.wrong_key.minisig -t $'timestamp:10\tfile:server_list.json\thashed' -s wrong_secret.key & -wait - -./generate_forged.py diff --git a/src/test_data/generate_forged.py b/src/test_data/generate_forged.py deleted file mode 100644 index 843b32d..0000000 --- a/src/test_data/generate_forged.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 - -import hashlib -import base64 - -# Hash server_list.json - -with open("server_list.json", "rb") as f: - b = f.read() - -with open("server_list.json.blake2b", "wb") as f: - f.write(hashlib.blake2b(b).digest()) - -# Forge pure signature on hash, see https://github.com/jedisct1/minisign/issues/104 - -with open("server_list.json.minisig", "rb") as f: - siglines = f.readlines() - -siglines[0] = b"untrusted comment: this signature has ED changed to Ed\n" -sig = base64.b64decode(siglines[1]) -siglines[1] = base64.b64encode(b"Ed" + sig[2:]) + b"\n" - -with open("server_list.json.forged_pure.minisig", "wb") as f: - f.writelines(siglines) - # Should now work: minisign -Vm server_list.json.blake2b -x server_list.json.forged_pure.minisig -p public-key - -# Try to forge key ID - -with open("server_list.json.wrong_key.minisig", "rb") as f: - siglines = f.readlines() - -siglines[0] = b"untrusted comment: this signature was created with wrong_secret.key but has key ID changed to that of public.key\n" -sig_wrong = base64.b64decode(siglines[1]) -siglines[1] = base64.b64encode(sig_wrong[:2] + sig[2:2+8] + sig_wrong[2+8:]) + b"\n" - -with open("server_list.json.forged_keyid.minisig", "wb") as f: - f.writelines(siglines) diff --git a/src/test_data/organization_list.json b/src/test_data/organization_list.json deleted file mode 100644 index 8c53044..0000000 --- a/src/test_data/organization_list.json +++ /dev/null @@ -1 +0,0 @@ -{"organization_list": [{}]}
\ No newline at end of file diff --git a/src/test_data/organization_list.json.minisig b/src/test_data/organization_list.json.minisig deleted file mode 100644 index 1fa546e..0000000 --- a/src/test_data/organization_list.json.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH31cHjNvTEh+TCqDVCwUgFVZoRdgWYAaQDxH3L3UIsRi9Qb1O4vLI4V1CYPatKzXZnSodSJM/AZgl9v7l/5bfPQ0= -trusted comment: timestamp:10 file:organization_list.json hashed -21zZv1DviMpLCdv1NgzLBl6d+F1ZllSNyjAquYxhTHGcs2F64bDFpqY0I0xjCHIoXly6HKqJKIBXNgud12ijCQ== diff --git a/src/test_data/organization_list.json.tc_servlist.minisig b/src/test_data/organization_list.json.tc_servlist.minisig deleted file mode 100644 index a7fe41f..0000000 --- a/src/test_data/organization_list.json.tc_servlist.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH31cHjNvTEh+TCqDVCwUgFVZoRdgWYAaQDxH3L3UIsRi9Qb1O4vLI4V1CYPatKzXZnSodSJM/AZgl9v7l/5bfPQ0= -trusted comment: timestamp:10 file:server_list.json hashed -R6hjM/oMS5LAvpYM4F6E7iUpnlPxqiY0QfuOnpum31CW0sUy/Ypy2PiomSwvZXKVR7keEZS/+lZjyra9TkrLDQ== diff --git a/src/test_data/other_list.json b/src/test_data/other_list.json deleted file mode 100644 index 25ba1a8..0000000 --- a/src/test_data/other_list.json +++ /dev/null @@ -1 +0,0 @@ -{"other_list": [{}]}
\ No newline at end of file diff --git a/src/test_data/other_list.json.minisig b/src/test_data/other_list.json.minisig deleted file mode 100644 index eaa2248..0000000 --- a/src/test_data/other_list.json.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH366C1RnYeUAgEeX/S5A1Z9qmkV2+GJaVj06FWGd4aMLc+HS7iFMhG69u3TVD4YmzMH12rk7hQrnyCC6ex8ypIQA= -trusted comment: timestamp:10 file:other_list.json hashed -26+608n+bjQF9lwNdXbIK6t5bP8dzhjNQ9hACeYJLiB2tr437Aec2GkmJh0jSiRv1QV4RYBcKJeHQBUcV2grCQ== diff --git a/src/test_data/public.key b/src/test_data/public.key deleted file mode 100644 index 72676d3..0000000 --- a/src/test_data/public.key +++ /dev/null @@ -1,2 +0,0 @@ -untrusted comment: minisign public key DF07F868DFAB9B4C -RWRMm6vfaPgH39iT++NBiUKZim2nDWnalgkNROovPbZdSwVFgUdKU4ac diff --git a/src/test_data/random.txt b/src/test_data/random.txt deleted file mode 100644 index b6fc4c6..0000000 --- a/src/test_data/random.txt +++ /dev/null @@ -1 +0,0 @@ -hello
\ No newline at end of file diff --git a/src/test_data/secret.key b/src/test_data/secret.key deleted file mode 100644 index 6e4af37..0000000 --- a/src/test_data/secret.key +++ /dev/null @@ -1,2 +0,0 @@ -untrusted comment: minisign encrypted secret key -RWRTY0IyobkTOt4ugAHNTPB6zOxHgX8spW6HQWddB5IrdCPDAgsAAAACAAAAAAAAAEAAAAAAvK1S1gsOgozZHuIdLWXq1IwxnWVr+dlySiykTbO6F85HvzPtgxZ7oLcGkT/vPdskAh0SV9H2ylHlt9oarXcWNDKs2r6EcZw/qy5FsD+5uhPfxwWV4qDF+1G456tYDYID63d50CgzdO0= diff --git a/src/test_data/server_list.json b/src/test_data/server_list.json deleted file mode 100644 index 67c4c8d..0000000 --- a/src/test_data/server_list.json +++ /dev/null @@ -1,3 +0,0 @@ -{ -"server_list": [{}] -}
\ No newline at end of file diff --git a/src/test_data/server_list.json.blake2b b/src/test_data/server_list.json.blake2b Binary files differdeleted file mode 100644 index 5d2ca5a..0000000 --- a/src/test_data/server_list.json.blake2b +++ /dev/null diff --git a/src/test_data/server_list.json.forged_keyid.minisig b/src/test_data/server_list.json.forged_keyid.minisig deleted file mode 100644 index efa349d..0000000 --- a/src/test_data/server_list.json.forged_keyid.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: this signature was created with wrong_secret.key but has key ID changed to that of public.key -RURMm6vfaPgH35aarz3NMq4gbv6JvzOnjG003bDe6USu+HT/JzuxHjQcQGE/KBPdyCF6BDDwwFu+NVmi5jotYCJHWOEqSBU70gE= -trusted comment: timestamp:10 file:server_list.json hashed -3BWYJamM3t6ImuXQufTeO81UMZNyM7TujMu7SCmR+oapsSEBpmkazGOgzlJYR53HP9K9zrEA+4lV8gFFngooBA== diff --git a/src/test_data/server_list.json.forged_pure.minisig b/src/test_data/server_list.json.forged_pure.minisig deleted file mode 100644 index a362504..0000000 --- a/src/test_data/server_list.json.forged_pure.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: this signature has ED changed to Ed -RWRMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:10 file:server_list.json hashed -oK41aX7rmpbO2ohF3v3+JGgSexQaVlfWvYPzaKEkDlJm8mVZtuK/h26SCRuL6PbTR92DLZU59rw8ckICUH/ADw== diff --git a/src/test_data/server_list.json.large_time.minisig b/src/test_data/server_list.json.large_time.minisig deleted file mode 100644 index 79a2a52..0000000 --- a/src/test_data/server_list.json.large_time.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:4300000000 file:server_list.json -L9C58LIq7bTLf4otqW4Eb+ASL0+FM7nRRjstCBuCPtuUerFIsOqNUpDp2AQJJ4pZJKE7SkgIq2tV8/IaVpzxBQ== diff --git a/src/test_data/server_list.json.minisig b/src/test_data/server_list.json.minisig deleted file mode 100644 index 143585b..0000000 --- a/src/test_data/server_list.json.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:10 file:server_list.json hashed -oK41aX7rmpbO2ohF3v3+JGgSexQaVlfWvYPzaKEkDlJm8mVZtuK/h26SCRuL6PbTR92DLZU59rw8ckICUH/ADw== diff --git a/src/test_data/server_list.json.pure.minisig b/src/test_data/server_list.json.pure.minisig deleted file mode 100644 index 57dccfc..0000000 --- a/src/test_data/server_list.json.pure.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RWRMm6vfaPgH3zQ/rcq2GMsNz1SYySz+olupm0I+nzNpOkPyUHTBwig3Pep4biOk/bH73bH+0sLNoZPcDk1f2Acn8JINc9MWMw4= -trusted comment: timestamp:10 file:server_list.json -FZ0eA96SlADsMrSOUgStQJpmUnBGpPbRvNI/oaYhKrylu6jUcXOgsRu6571mmDxYdlruSuUSlQbdmG81Qbl4AA== diff --git a/src/test_data/server_list.json.tc_earliertime.minisig b/src/test_data/server_list.json.tc_earliertime.minisig deleted file mode 100644 index 03da710..0000000 --- a/src/test_data/server_list.json.tc_earliertime.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:9 file:server_list.json hashed -vw3wjLDNZWoV98/GnFv38REiaeh+wUPEZgmBUvY35CEq00jDdHiJcYRV/7zBoKv+n9TAYxZ8WKUOGWNOPonTBg== diff --git a/src/test_data/server_list.json.tc_emptyfile.minisig b/src/test_data/server_list.json.tc_emptyfile.minisig deleted file mode 100644 index a7aa3ed..0000000 --- a/src/test_data/server_list.json.tc_emptyfile.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:10 file: hashed -g4drZ91TcYXNLnIGbeH5ZIFzrs2wWB9JTXjV3Jwg9ehSC2D8lCTqw3u2Rg+PvLPRvYmXTHyuJoKNWelsSh64CA== diff --git a/src/test_data/server_list.json.tc_emptytime.minisig b/src/test_data/server_list.json.tc_emptytime.minisig deleted file mode 100644 index d3ef01e..0000000 --- a/src/test_data/server_list.json.tc_emptytime.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp: file:server_list.json hashed -lw5rnZsPi+TkZ4lOCy7bjsUgTXxG+jaGOGdHuNL95FSD2mmP9ZzEJPrJ2jnH7iYfkF3zDm0QvEUDxhEirlHBDA== diff --git a/src/test_data/server_list.json.tc_latertime.minisig b/src/test_data/server_list.json.tc_latertime.minisig deleted file mode 100644 index 8237123..0000000 --- a/src/test_data/server_list.json.tc_latertime.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:20 file:server_list.json hashed -rHcsHF2mmcZvDLreeuljVauuFULWiY8luCsxyBxxobcJkCedEDW3/RX5KeT+2NjHSFuQxkmrYOBWTY9+ECuUDQ== diff --git a/src/test_data/server_list.json.tc_nofile.minisig b/src/test_data/server_list.json.tc_nofile.minisig deleted file mode 100644 index 3c1dcbe..0000000 --- a/src/test_data/server_list.json.tc_nofile.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:10 hashed -NonaTZH7RDbsHXv85M7sL43YE7CTzs5qDRRoFYjajeqzHa+hdIuMGyemK85rAJ3prLGnMdWHkZhD4hsr3cZoDA== diff --git a/src/test_data/server_list.json.tc_nohashed.minisig b/src/test_data/server_list.json.tc_nohashed.minisig deleted file mode 100644 index 1d140c1..0000000 --- a/src/test_data/server_list.json.tc_nohashed.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:10 file:server_list.json -HaPGKT+Jqxjyw2Nt1GEKaPIZsAmVl/RI6p1mQ+S1LqzYicVgT5GxPs9NR6khdGGIFvo/xhVkXFceAWTRUCVQAg== diff --git a/src/test_data/server_list.json.tc_notime.minisig b/src/test_data/server_list.json.tc_notime.minisig deleted file mode 100644 index 39625c3..0000000 --- a/src/test_data/server_list.json.tc_notime.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: file:server_list.json hashed -dMhb+0Y0KAO2tzI4g0ukL/VdMiLVopmXa9BS1RQBY8bYwzmebdIM4DAIZrhtO1avkpdy0prZehuhA1No6cOSAw== diff --git a/src/test_data/server_list.json.tc_orglist.minisig b/src/test_data/server_list.json.tc_orglist.minisig deleted file mode 100644 index 7c2a3a8..0000000 --- a/src/test_data/server_list.json.tc_orglist.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:10 file:organization_list.json hashed -NreDM4iGEjMWs5sfaJCGZBZ7D9QLqxBKJ/fVW2lvIDr249DSUNR4ZRca8UL73e3c9eTXgHnY/ojsjDtzxDScDw== diff --git a/src/test_data/server_list.json.tc_otherfile.minisig b/src/test_data/server_list.json.tc_otherfile.minisig deleted file mode 100644 index 58a29b2..0000000 --- a/src/test_data/server_list.json.tc_otherfile.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: timestamp:10 file:otherfile hashed -PfDEIMlt2aNFyOnqHb45S7xm4fIg0vfUUbqXENPxry9GEZFX14c5BGtgcL/krDg8WFJHcIA5bzYcX58kgBiZCA== diff --git a/src/test_data/server_list.json.tc_random.minisig b/src/test_data/server_list.json.tc_random.minisig deleted file mode 100644 index 7240980..0000000 --- a/src/test_data/server_list.json.tc_random.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURMm6vfaPgH3997FX/cHwhXJpcluwbNiznrfYV83WS/Gsd3BeO/g10Mo7Z9N5rMSXcpGrmT2CagiEEm5zSw/MEnTqs4YWICdQs= -trusted comment: random stuff -szGsyESH0EizTXH6n0yuQg6sHTKXr+TJW/Er9ZNJYgQV+1hVM+fc5q1EmVsJlA3kW4Rt/d1p9F0ShLIIgW2vAA== diff --git a/src/test_data/server_list.json.wrong_key.minisig b/src/test_data/server_list.json.wrong_key.minisig deleted file mode 100644 index 5a83c0e..0000000 --- a/src/test_data/server_list.json.wrong_key.minisig +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RUTQvDHvQuYCCJaarz3NMq4gbv6JvzOnjG003bDe6USu+HT/JzuxHjQcQGE/KBPdyCF6BDDwwFu+NVmi5jotYCJHWOEqSBU70gE= -trusted comment: timestamp:10 file:server_list.json hashed -3BWYJamM3t6ImuXQufTeO81UMZNyM7TujMu7SCmR+oapsSEBpmkazGOgzlJYR53HP9K9zrEA+4lV8gFFngooBA== diff --git a/src/test_data/wrong_public.key b/src/test_data/wrong_public.key deleted file mode 100644 index aa794d4..0000000 --- a/src/test_data/wrong_public.key +++ /dev/null @@ -1,2 +0,0 @@ -untrusted comment: minisign public key 802E642EF31BCD0 -RWTQvDHvQuYCCPDLi3UCXzj3BbzFM5QxUFfrp174iaqYo8lT0VaAkhOt diff --git a/src/test_data/wrong_secret.key b/src/test_data/wrong_secret.key deleted file mode 100644 index 68e9092..0000000 --- a/src/test_data/wrong_secret.key +++ /dev/null @@ -1,2 +0,0 @@ -untrusted comment: minisign encrypted secret key -RWRTY0Iyrc2CTG2W1ZqEq9tb94oQWTnYUy4k8boMf13478FwlDYAAAACAAAAAAAAAEAAAAAA2gFhwOtjETu5WN1LpgtJHV1dk/7466LBJ8dgO/pZoQ3LLAYxlswJHVR/N/Q1HmmKvlxWo2jNTJcARXuHHlMTEgg1MERTldE88CqETrVbvq1JaqJlAY/HMkiqNEUR3L6+5VHbYPKXlVQ= diff --git a/src/util.go b/src/util.go deleted file mode 100644 index 87340f9..0000000 --- a/src/util.go +++ /dev/null @@ -1,21 +0,0 @@ -package eduvpn - -import ( - "crypto/rand" - "time" -) - -// Creates a random byteslice of `size` -func MakeRandomByteSlice(size int) ([]byte, error) { - byteSlice := make([]byte, size) - _, err := rand.Read(byteSlice) - if err != nil { - return nil, err - } - return byteSlice, nil -} - -func GenerateTimeSeconds() int64 { - current := time.Now() - return current.Unix() -} diff --git a/src/verify.go b/src/verify.go deleted file mode 100644 index 3c87fe1..0000000 --- a/src/verify.go +++ /dev/null @@ -1,205 +0,0 @@ -package eduvpn - -import ( - "errors" - "fmt" - "os" - - "github.com/jedisct1/go-minisign" -) - -// 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. -// 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) { - 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, forcePrehash) - if err != nil { - var verifyCreatePublickeyError *VerifyCreatePublicKeyError - if errors.As(err, &verifyCreatePublickeyError) { - panic(err) // This should not happen unless keyStrs has an invalid key - } - return valid, err - } - 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 -} - -type VerifyUnknownExpectedFilenameError struct { - Filename string - Expected string -} - -func (e *VerifyUnknownExpectedFilenameError) Error() string { - return fmt.Sprintf("invalid filename %s, expected %s", e.Filename, e.Expected) -} - -type VerifyInvalidSignatureFormatError struct { - Err error -} - -func (e *VerifyInvalidSignatureFormatError) Error() string { - return fmt.Sprintf("invalid signature format, error %v", e.Err) -} - -type VerifyInvalidSignatureAlgorithmError struct { - Algorithm string - WantedAlgorithm string -} - -func (e *VerifyInvalidSignatureAlgorithmError) Error() string { - return fmt.Sprintf("invalid signature algorithm %s, wanted %s", e.Algorithm, e.WantedAlgorithm) -} - -type VerifyCreatePublicKeyError struct { - PublicKey string - Err error -} - -func (e *VerifyCreatePublicKeyError) Error() string { - return fmt.Sprintf("failed to create public key %s with error %v", e.PublicKey, e.Err) -} - -type VerifyInvalidSignatureError struct { - Err error -} - -func (e *VerifyInvalidSignatureError) Error() string { - return fmt.Sprintf("invalid signature with error %v", e.Err) -} - -type VerifyInvalidTrustedCommentError struct { - TrustedComment string - Err error -} - -func (e *VerifyInvalidTrustedCommentError) Error() string { - return fmt.Sprintf("invalid trusted comment %s with error %v", e.TrustedComment, e.Err) -} - -type VerifyWrongSigFilenameError struct { - Filename string - SigFilename string -} - -func (e *VerifyWrongSigFilenameError) Error() string { - return fmt.Sprintf("wrong filename %s, expected filename %s for signature", e.Filename, e.SigFilename) -} - -type VerifySigTimeEarlierError struct { - SigTime uint64 - MinSigTime uint64 -} - -func (e *VerifySigTimeEarlierError) Error() string { - return fmt.Sprintf("Sign time %d is earlier than sign time %d", e.SigTime, e.MinSigTime) -} - -type VerifyUnknownKeyError struct { - Filename string -} - -func (e *VerifyUnknownKeyError) Error() string { - return fmt.Sprintf("signature for filename %s was created with an unknown key", e.Filename) -} - -// 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. -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, &VerifyUnknownExpectedFilenameError{Filename: filename, Expected: "server_list.json or organization_list.json"} - } - - sig, err := minisign.DecodeSignature(signatureFileContent) - if err != nil { - return false, &VerifyInvalidSignatureFormatError{Err: err} - } - - // Check if signature is prehashed, see https://jedisct1.github.io/minisign/#signature-format - if forcePrehash && sig.SignatureAlgorithm != [2]byte{'E', 'D'} { - return false, &VerifyInvalidSignatureAlgorithmError{Algorithm: string(sig.SignatureAlgorithm[:]), WantedAlgorithm: "ED (BLAKE2b-prehashed EdDSA)"} - } - - // 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, &VerifyCreatePublicKeyError{PublicKey: keyStr, Err: err} - } - - if sig.KeyId != key.KeyId { - continue // Wrong key - } - - valid, err := key.Verify(signedJson, sig) - if !valid { - return false, &VerifyInvalidSignatureError{Err: 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, &VerifyInvalidTrustedCommentError{TrustedComment: sig.TrustedComment, Err: err} - } - - if sigFileName != filename { - return false, &VerifyWrongSigFilenameError{Filename: filename, SigFilename: sigFileName} - } - - if signTime < minSignTime { - return false, &VerifySigTimeEarlierError{SigTime: signTime, MinSigTime: minSignTime} - } - - return true, nil - } - - // No matching allowed key found - return false, &VerifyUnknownKeyError{Filename: filename} -} diff --git a/src/verify_test.go b/src/verify_test.go deleted file mode 100644 index fc78ec3..0000000 --- a/src/verify_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package eduvpn - -import ( - "bufio" - "errors" - "fmt" - "io/ioutil" - "os" - "testing" -) - -func Test_verifyWithKeys(t *testing.T) { - var err error - - var pk []string - { - file, err := os.Open("test_data/public.key") - if err != nil { - panic(err) - } - defer file.Close() - - // Get last line (key string) from file - scanner := bufio.NewScanner(file) - for i := 0; i < 2; i++ { - if !scanner.Scan() { - panic(scanner.Err()) - } - } - pk = []string{scanner.Text()} - } - - var ( - verifyCreatePublicKeyError *VerifyCreatePublicKeyError - verifyInvalidSignatureAlgorithmError *VerifyInvalidSignatureAlgorithmError - verifyWrongSigFilenameError *VerifyWrongSigFilenameError - verifyInvalidTrustedCommentError *VerifyInvalidTrustedCommentError - verifyInvalidSignatureFormatError *VerifyInvalidSignatureFormatError - verifyInvalidSignatureError *VerifyInvalidSignatureError - verifySigTimeEarlierError *VerifySigTimeEarlierError - verifyUnknownExpectedFilenameError *VerifyUnknownExpectedFilenameError - verifyUnknownKeyError *VerifyUnknownKeyError - ) - - tests := []struct { - expectedErr interface{} - testName string - signatureFile string - jsonFile string - expectedFileName string - minSignTime uint64 - allowedPks []string - }{ - {&verifyInvalidSignatureAlgorithmError, "pure", "server_list.json.pure.minisig", "server_list.json", "server_list.json", 10, pk}, - - {nil, "valid server_list", "server_list.json.minisig", "server_list.json", "server_list.json", 10, pk}, - {nil, "TC no hashed", "server_list.json.tc_nohashed.minisig", "server_list.json", "server_list.json", 10, pk}, - {nil, "TC later time", "server_list.json.tc_latertime.minisig", "server_list.json", "server_list.json", 10, pk}, - {&verifyWrongSigFilenameError, "server_list TC file:organization_list", "server_list.json.tc_orglist.minisig", "server_list.json", "server_list.json", 10, pk}, - {&verifyWrongSigFilenameError, "organization_list as server_list", "organization_list.json.minisig", "organization_list.json", "server_list.json", 10, pk}, - {&verifyWrongSigFilenameError, "TC file:otherfile", "server_list.json.tc_otherfile.minisig", "server_list.json", "server_list.json", 10, pk}, - {&verifySigTimeEarlierError, "TC no file", "server_list.json.tc_nofile.minisig", "server_list.json", "server_list.json", 10, pk}, - {&verifySigTimeEarlierError, "TC no time", "server_list.json.tc_notime.minisig", "server_list.json", "server_list.json", 10, pk}, - {&verifySigTimeEarlierError, "TC empty time", "server_list.json.tc_emptytime.minisig", "server_list.json", "server_list.json", 10, pk}, - {&verifyInvalidSignatureFormatError, "TC empty file", "server_list.json.tc_emptyfile.minisig", "server_list.json", "server_list.json", 10, pk}, - {&verifyInvalidTrustedCommentError, "TC random", "server_list.json.tc_random.minisig", "server_list.json", "server_list.json", 10, pk}, - {nil, "large time", "server_list.json.large_time.minisig", "server_list.json", "server_list.json", 43e8, pk}, - {nil, "lower min time", "server_list.json.minisig", "server_list.json", "server_list.json", 5, pk}, - {&verifySigTimeEarlierError, "higher min time", "server_list.json.minisig", "server_list.json", "server_list.json", 11, pk}, - - {nil, "valid organization_list", "organization_list.json.minisig", "organization_list.json", "organization_list.json", 10, pk}, - {&verifyWrongSigFilenameError, "organization_list TC file:server_list", "organization_list.json.tc_servlist.minisig", "organization_list.json", "organization_list.json", 10, pk}, - {&verifyWrongSigFilenameError, "server_list as organization_list", "server_list.json.minisig", "server_list.json", "organization_list.json", 10, pk}, - - {&verifyUnknownExpectedFilenameError, "valid other_list", "other_list.json.minisig", "other_list.json", "other_list.json", 10, pk}, - {&verifyWrongSigFilenameError, "other_list as server_list", "other_list.json.minisig", "other_list.json", "server_list.json", 10, pk}, - - {&verifyInvalidSignatureFormatError, "invalid signature file", "random.txt", "server_list.json", "server_list.json", 10, pk}, - {&verifyInvalidSignatureFormatError, "empty signature file", "empty", "server_list.json", "server_list.json", 10, pk}, - - {&verifyUnknownKeyError, "wrong key", "server_list.json.wrong_key.minisig", "server_list.json", "server_list.json", 10, pk}, - - {&verifyInvalidSignatureAlgorithmError, "forged pure signature", "server_list.json.forged_pure.minisig", "server_list.json.blake2b", "server_list.json", 10, pk}, - {&verifyInvalidSignatureError, "forged key ID", "server_list.json.forged_keyid.minisig", "server_list.json", "server_list.json", 10, pk}, - - {&verifyUnknownKeyError, "no allowed keys", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{}}, - {nil, "multiple allowed keys 1", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{ - pk[0], "RWSf0PYToIUJmDlsz21YOXvgQzHj9NSdyJUqEY5ZdfS9GepeXt3+JJRZ", - }}, - {nil, "multiple allowed keys 2", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{ - "RWSf0PYToIUJmDlsz21YOXvgQzHj9NSdyJUqEY5ZdfS9GepeXt3+JJRZ", pk[0], - }}, - {&verifyCreatePublicKeyError, "invalid allowed key", "server_list.json.minisig", "server_list.json", "server_list.json", 10, []string{"AAA"}}, - } - - // Cache file contents in map, mapping file names to contents - files := map[string][]byte{} - loadFile := func(name string) { - content, loaded := files[name] - if !loaded { - content, err = ioutil.ReadFile("test_data/" + name) - if err != nil { - panic(err) - } - files[name] = content - } - } - for _, test := range tests { - loadFile(test.signatureFile) - loadFile(test.jsonFile) - } - - forcePrehash := true - for _, tt := range tests { - t.Run(tt.testName, func(t *testing.T) { - t.Parallel() - valid, err := verifyWithKeys(string(files[tt.signatureFile]), files[tt.jsonFile], - tt.expectedFileName, tt.minSignTime, tt.allowedPks, forcePrehash) - compareResults(t, valid, err, tt.expectedErr, func() string { - return fmt.Sprintf("verifyWithKeys(%q, %q, %q, %v, %v, %t)", - tt.signatureFile, tt.jsonFile, tt.expectedFileName, tt.minSignTime, tt.allowedPks, forcePrehash) - }) - }) - } -} - -// compareResults compares returned ret, err from a verify function with expected error code expected. -// callStr is called to get the formatted parameters passed to the function. -func compareResults(t *testing.T, ret bool, err error, expectedErr interface{}, callStr func() string) { - // different error returned - if expectedErr != nil && !errors.As(err, expectedErr) { - t.Errorf("%v\nerror %T = %v, wantErr %T", callStr(), err, err, expectedErr) - return - } - // different boolean returned - expectedBool := expectedErr == nil - if ret != expectedBool { - t.Errorf("%v\n= %v, want %v", callStr(), ret, expectedBool) - } -} diff --git a/src/wireguard.go b/src/wireguard.go deleted file mode 100644 index 2f1c41c..0000000 --- a/src/wireguard.go +++ /dev/null @@ -1,52 +0,0 @@ -package eduvpn - -import ( - "fmt" - "regexp" - - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" -) - -func wireguardGenerateKey() (wgtypes.Key, error) { - key, error := wgtypes.GeneratePrivateKey() - return key, error -} - -// FIXME: Instead of doing a regex replace, decide if we should use a parser -func wireguardConfigAddKey(config string, key wgtypes.Key) string { - interface_section := "[Interface]" - interface_section_escaped := regexp.QuoteMeta(interface_section) - - // (?m) enables multi line mode - // ^ match from beginning of line - // $ match till end of line - // So it matches [Interface] section exactly - interface_re := regexp.MustCompile(fmt.Sprintf("(?m)^%s$", interface_section_escaped)) - to_replace := fmt.Sprintf("%s\nPrivateKey = %s", interface_section, key.String()) - return interface_re.ReplaceAllString(config, to_replace) -} - -func (server *Server) WireguardGetConfig() (string, error) { - profile_id := server.Profiles.Current - wireguardKey, wireguardErr := wireguardGenerateKey() - - if wireguardErr != nil { - return "", wireguardErr - } - - wireguardPublicKey := wireguardKey.PublicKey().String() - configWireguard, _, configErr := server.APIConnectWireguard(profile_id, wireguardPublicKey) - - if configErr != nil { - return "", configErr - } - - // FIXME: Store expiry - // This needs the go code a way to identify a connection - // Use the uuid of the connection e.g. on Linux - // This needs the client code to call the go code - - configWireguardKey := wireguardConfigAddKey(configWireguard, wireguardKey) - - return configWireguardKey, nil -} |
