diff --git a/config/config.go b/config/config.go index bdcffc1..a3adeb4 100644 --- a/config/config.go +++ b/config/config.go @@ -52,7 +52,9 @@ type ConfigAdmin struct { ProfilingPassword string `toml:"profiling_password"` // Allow registration on the server // If disabled, user must be manually created (currently via the debug server) - AllowRegistration bool `toml:"allow_registration"` + AllowRegistration bool `toml:"allow_registration"` + AuthFetchForNonGet bool `toml:"auth_fetch_for_non_get"` + AuthFetchForGet bool `toml:"auth_fetch_for_get"` } type ConfigStorage struct { @@ -119,6 +121,10 @@ type ConfigExperimental struct { // Both are created and stored for each local user. If this flag is enabled, // Linstrom shares the ED25519 key on request, otherwise the RSA key UseEd25519Keys bool `toml:"use_ed25519_keys"` + // Require authorized fetch signing for requests to the server actor too + // The implementation itself is stable, but might cause issues during initial connect + // if the other server also requires authorized fetch for the server actor + AuthFetchForServerActor bool `toml:"auth_fetch_for_server_actor"` } type Config struct { @@ -151,10 +157,12 @@ var defaultConfig Config = Config{ AdminMail: nil, }, Admin: ConfigAdmin{ - Username: "server-admin", - FirstTimeSetupOTP: "Example otp password", - ProfilingPassword: "Example profiling password", - AllowRegistration: true, + Username: "server-admin", + FirstTimeSetupOTP: "Example otp password", + ProfilingPassword: "Example profiling password", + AllowRegistration: true, + AuthFetchForNonGet: true, + AuthFetchForGet: false, }, Webauthn: ConfigWebAuthn{ DisplayName: "Linstrom", @@ -198,7 +206,8 @@ var defaultConfig Config = Config{ UseSSL: false, }, Experimental: ConfigExperimental{ - UseEd25519Keys: false, + UseEd25519Keys: false, + AuthFetchForServerActor: false, }, } diff --git a/devserver/linstrom.toml b/devserver/linstrom.toml index 47f9263..4c50a14 100644 --- a/devserver/linstrom.toml +++ b/devserver/linstrom.toml @@ -11,6 +11,8 @@ first_time_setup_otp = "Example otp password" profiling_password = "" allow_registration = true + auth_fetch_for_get = false + auth_fetch_for_non_get = true [webauthn] display_name = "Linstrom" @@ -47,3 +49,4 @@ [experimental] use_ed25519_keys = false + auth_fetch_for_server_actor = false diff --git a/main.go b/main.go index 001564f..ead390d 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ package main import ( "embed" "flag" + "fmt" "path" "time" @@ -163,6 +164,11 @@ func newServer() { }() } log.Info().Msg("Starting public server") - public := webpublic.New(":8080", &defaultDuck) - public.Start() + public := webpublic.New( + fmt.Sprintf(":%v", config.GlobalConfig.General.PrivatePort), + &defaultDuck, + ) + if err = public.Start(); err != nil { + log.Fatal().Err(err).Msg("Failed to start public server") + } } diff --git a/temp.toml b/temp.toml index b3cf406..bffcb29 100644 --- a/temp.toml +++ b/temp.toml @@ -1,7 +1,8 @@ +# . [general] protocol = "https" domain = "serveo.net" - subdomain = "38e5543b0fc97680472709952a04622f" + subdomain = "5dc9c90be02fecb3b132ad4d4877555a" private_port = 8080 public_port = 443 @@ -13,6 +14,8 @@ first_time_setup_otp = "Example otp password" profiling_password = "" allow_registration = true + auth_fetch_for_get = false + auth_fetch_for_non_get = true [webauthn] display_name = "Linstrom" @@ -49,3 +52,4 @@ [experimental] use_ed25519_keys = false + auth_fetch_for_server_actor = false diff --git a/web/public/api/api.go b/web/public/api/api.go index 3570a43..ba00ec8 100644 --- a/web/public/api/api.go +++ b/web/public/api/api.go @@ -6,6 +6,7 @@ import ( webutils "git.mstar.dev/mstar/goutils/http" + "git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/web/public/api/activitypub" webmiddleware "git.mstar.dev/mstar/linstrom/web/public/middleware" ) @@ -18,7 +19,10 @@ func BuildApiRouter() http.Handler { "/activitypub", webutils.ChainMiddlewares( activitypub.BuildActivitypubRouter(), - webmiddleware.BuildAuthorizedFetchCheck(true, true), + webmiddleware.BuildAuthorizedFetchCheck( + config.GlobalConfig.Admin.AuthFetchForNonGet, + config.GlobalConfig.Admin.AuthFetchForGet, + ), ), ), ) diff --git a/web/public/middleware/authFetchCheck.go b/web/public/middleware/authFetchCheck.go index 485b05b..d87e920 100644 --- a/web/public/middleware/authFetchCheck.go +++ b/web/public/middleware/authFetchCheck.go @@ -5,10 +5,12 @@ import ( "crypto/rsa" "crypto/sha256" "crypto/x509" + "encoding/base64" "net/http" "net/url" "regexp" "strings" + "time" webutils "git.mstar.dev/mstar/goutils/http" "git.mstar.dev/mstar/goutils/other" @@ -17,12 +19,13 @@ import ( "git.mstar.dev/mstar/linstrom/activitypub" "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/storage-new" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" webshared "git.mstar.dev/mstar/linstrom/web/shared" ) -const signatureRegexString = `keyId="([a-zA-Z0-9_\-\.:@\/\?&=#%\+\[\]!$\(\)\*,;]+)",(?:algorithm="([a-z0-9-]+)",)?headers="([a-z0-9-_\(\) ]+)",(?:algorithm="([a-z0-9-]+)",)?signature="(.+)"` +const signatureRegexString = `keyId="([a-zA-Z0-9_\-\.:@\/\?&=#%\+\[\]!$\(\)\*,;]+)",(?:algorithm="([a-z0-9-]+)",)?headers="([a-z0-9\-_\(\) ]+)",(?:algorithm="([a-z0-9-]+)",)?signature="(.+)"` var publicPaths = []*regexp.Regexp{ regexp.MustCompile(`/\.well-known/.+^`), @@ -61,12 +64,12 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil // So either you get the server actor, which is intended behaviour, or access to // one specific note that most likely won't even exist - // FIXME: Re-enable once implementation of actual verification is stable - // if strings.Contains(path, storage.ServerActorId) { - // log.Info().Msg("Server actor requested, no auth") - // h.ServeHTTP(w, r) - // return - // } + if !config.GlobalConfig.Experimental.AuthFetchForServerActor && + strings.Contains(path, storage.ServerActorId) { + log.Info().Msg("Server actor requested, no auth") + h.ServeHTTP(w, r) + return + } // Not an always open path, check methods if r.Method == "GET" && !forGet { log.Info().Msg("Get request to AP resources don't need signature") @@ -78,6 +81,30 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil return } log.Info().Msg("Need signature for AP request") + rawDate := r.Header.Get("Date") + date, err := http.ParseTime(rawDate) + if err != nil { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-date", + "no or invalid date header", + nil, + nil, + ) + return + } + if time.Since(date) > time.Hour+time.Minute*5 { + webutils.ProblemDetails( + w, + http.StatusUnauthorized, + "/errors/invalid-auth-signature", + "invalid authorization signature", + other.IntoPointer("Request is outdated"), + nil, + ) + return + } signatureHeader := r.Header.Get("Signature") if signatureHeader == "" { log.Info().Msg("Received AP request without signature header where one is required") @@ -117,7 +144,20 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil rawAlgorithm1 := match[2] rawHeaders := match[3] rawAlgorithm2 := match[4] - signature := match[5] + signature, err := base64.StdEncoding.DecodeString(match[5]) + if err != nil { + webutils.ProblemDetails( + w, + http.StatusUnauthorized, + "/errors/invalid-auth-signature", + "invalid authorization signature", + other.IntoPointer( + "Signature not decodable as bas64", + ), + nil, + ) + return + } var rawAlgorithm string if rawAlgorithm2 != "" { @@ -127,7 +167,7 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil } else { rawAlgorithm = "hs2019" } - _, err := url.Parse(rawKeyId) + _, err = url.Parse(rawKeyId) if err != nil { log.Warn().Err(err).Msg("Key id is not an url") webutils.ProblemDetails( diff --git a/web/shared/client.go b/web/shared/client.go index 5ccd00a..fcb9c05 100644 --- a/web/shared/client.go +++ b/web/shared/client.go @@ -3,11 +3,8 @@ package webshared import ( "io" "net/http" - "strings" "time" - "git.mstar.dev/mstar/goutils/maputils" - "git.mstar.dev/mstar/linstrom/config" ) @@ -28,18 +25,7 @@ func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte headers.Set("Date", time.Now().UTC().Format(http.TimeFormat)) } applyBodyHash(headers, postBody) - mappedHeaders := maputils.MapNewKeys(headers, func(k string, v []string) (string, string) { - if len(v) > 0 { - return strings.ToLower(k), v[0] - } else { - return strings.ToLower(k), "" - } - }) // Filter for only the date, host, digest and request-target headers - mappedHeaders = maputils.FilterMap(mappedHeaders, func(k, v string) bool { - k = strings.ToLower(k) - return k == "date" || k == "host" || k == "digest" || k == "(request-target)" - }) var signedString string var usedHeaders []string if config.GlobalConfig.Experimental.UseEd25519Keys { diff --git a/web/shared/signing.go b/web/shared/signing.go index e776ae6..4adb0de 100644 --- a/web/shared/signing.go +++ b/web/shared/signing.go @@ -6,7 +6,6 @@ import ( "net/url" "strings" - "git.mstar.dev/mstar/goutils/maputils" "github.com/rs/zerolog/log" "git.mstar.dev/mstar/linstrom/config" @@ -61,7 +60,10 @@ func genPreSignatureString( headers http.Header, ) (string, []string) { usedHeaders := []string{"(request-target)", "host"} - usedHeaders = append(usedHeaders, maputils.KeysFromMap(headers)...) + usedHeaders = append(usedHeaders, "date", "accept", "content-type") + if headers.Get("Digest") != "" { + usedHeaders = append(usedHeaders, "digest") + } return GenerateStringToSign(method, target.Host, target.Path, headers, usedHeaders), usedHeaders } @@ -81,7 +83,7 @@ func GenerateStringToSign( case "host": dataBuilder.WriteString(v + ": " + host + "\n") default: - dataBuilder.WriteString(v + ": " + headers.Get(v) + "\n") + dataBuilder.WriteString(strings.ToLower(v) + ": " + headers.Get(v) + "\n") } // dataBuilder.WriteString(k + ": " + v + "\n") // usedHeaders = append(usedHeaders, k)