package webmiddleware import ( "crypto" "crypto/rsa" "crypto/sha256" "crypto/x509" "net/http" "net/url" "regexp" "strings" webutils "git.mstar.dev/mstar/goutils/http" "git.mstar.dev/mstar/goutils/other" "github.com/rs/zerolog/hlog" "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" "git.mstar.dev/mstar/linstrom/config" "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="(.+)"` var publicPaths = []*regexp.Regexp{ regexp.MustCompile(`/\.well-known/.+^`), // regexp.MustCompile(``), } var signatureRegex = regexp.MustCompile(signatureRegexString) // Builder for the authorized fetch check middleware. // forNonGet determines whether the check should happen for non-GET requests. // forGet determines whether the check should also happen for GET requests. // forGet being true implicitly sets forNonGet true too. // Requests to the server actor and other, required public resources // will never be checked func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuilder { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log := hlog.FromRequest(r) log.Info().Msg("AuthFetch middleware") defer log.Info().Msg("AuhFetch completed") path := r.URL.Path // Check always open path first for _, re := range publicPaths { if re.Match([]byte(path)) { h.ServeHTTP(w, r) return } } // WARN: This check could potentially pose a security risk where an AP request // could get around providing a valid signature by using the server actor's // ID somewhere in the path. // However, I suspect that this risk is mostly mitigated by Go's router // already cleaning paths up and the only other target being one specific // note that most likely won't ever even exist due to the large random value space // offered by UUIDs // 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 // } // Not an always open path, check methods if r.Method == "GET" && !forGet { log.Info().Msg("Get request to AP resources don't need signature") h.ServeHTTP(w, r) return } else if !forGet && !forNonGet { log.Info().Msg("Requests to AP resources don't need signature") h.ServeHTTP(w, r) return } log.Info().Msg("Need signature for AP request") signatureHeader := r.Header.Get("Signature") if signatureHeader == "" { log.Info().Msg("Received AP request without signature header where one is required") webutils.ProblemDetails( w, http.StatusUnauthorized, "/errors/invalid-auth-signature", "invalid authorization signature", other.IntoPointer("Missing Signature header for authorization"), nil, ) return } log.Debug(). Str("signature-header", signatureHeader). Msg("Signature header of inbound AP request") match := signatureRegex.FindStringSubmatch(signatureHeader) if len(match) <= 1 { log.Info(). Str("header", signatureHeader). Msg("Received signature with invalid pattern") webutils.ProblemDetails( w, http.StatusUnauthorized, "/errors/invalid-auth-signature", "invalid authorization signature", other.IntoPointer("Invalid signature header format"), map[string]any{ "used signature regex": signatureRegexString, "signature format": `keyId="",headers="",(optional: algorithm="", (defaults to rsa-sha256 if not set))signature="signed message"`, }, ) return } // fullMatch = match[0] rawKeyId := match[1] rawAlgorithm1 := match[2] rawHeaders := match[3] rawAlgorithm2 := match[4] signature := match[5] var rawAlgorithm string if rawAlgorithm2 != "" { rawAlgorithm = rawAlgorithm2 } else if rawAlgorithm1 != "" { rawAlgorithm = rawAlgorithm1 } else { rawAlgorithm = "hs2019" } _, err := url.Parse(rawKeyId) if err != nil { log.Warn().Err(err).Msg("Key id is not an url") webutils.ProblemDetails( w, http.StatusUnauthorized, "/errors/invalid-auth-signature", "invalid authorization signature", other.IntoPointer("keyId must be a valid url"), nil, ) return } _ = rawAlgorithm stringToCheck := buildStringToCheck(r, rawHeaders) log.Warn().Str("string-to-check", stringToCheck).Send() requestingActor, err := getRequestingActor(rawKeyId) if err != nil { log.Error().Err(err).Msg("Failed to get requesting actor") webutils.ProblemDetails( w, http.StatusUnauthorized, "/errors/invalid-auth-signature", "invalid authorization signature", other.IntoPointer( "Failed to get the signing account for signature verification", ), nil, ) return } log.Debug(). Str("id", requestingActor.ID). Str("username", requestingActor.Username). Msg("Got requesting actor") key, err := x509.ParsePKCS1PublicKey(requestingActor.PublicKeyRsa) if err != nil { rawKey, err := x509.ParsePKIXPublicKey(requestingActor.PublicKeyRsa) if err != nil { log.Warn().Err(err).Msg("Failed to parse public key of requesting actor") webutils.ProblemDetails( w, http.StatusUnauthorized, "/errors/invalid-auth-signature", "invalid authorization signature", other.IntoPointer("Key is not a valid PKCS1 marshalled key"), nil, ) return } var ok bool key, ok = rawKey.(*rsa.PublicKey) if !ok { log.Warn().Msg("Received public key is not rsa") webutils.ProblemDetails( w, http.StatusUnauthorized, "/errors/invalid-auth-signature", "invalid authorization signature", other.IntoPointer("Received public key is not rsa"), nil, ) return } } hash := sha256.Sum256([]byte(stringToCheck)) err = rsa.VerifyPKCS1v15(key, crypto.SHA256, hash[:], []byte(signature)) if err != nil { log.Warn().Err(err).Msg("Signature verification failed") webutils.ProblemDetails( w, http.StatusUnauthorized, "/errors/invalid-auth-signature", "invalid authorization signature", other.IntoPointer( "Verification of the given signature with the user's public key failed", ), nil, ) return } h.ServeHTTP(w, r) }) } } func buildStringToCheck(r *http.Request, rawSignedHeaders string) string { headersToUse := strings.Split(rawSignedHeaders, " ") // Doesn't matter here if convert failed as path doesn't have prefix in that case path, ok := r.Context().Value((FullPathContextKey)).(string) if !ok { path = r.URL.Path } return webshared.GenerateStringToSign( r.Method, config.GlobalConfig.General.GetFullDomain(), path, r.Header, headersToUse, ) } func getRequestingActor(keyId string) (*models.User, error) { // Cut away key id to get AP url keyId = strings.Split(keyId, "#")[0] acc, err := dbgen.User.GetRemoteAccountByApUrl(keyId) switch err { case gorm.ErrRecordNotFound: acc, err = activitypub.ImportRemoteAccountByAPUrl(keyId) if err != nil { return nil, err } return acc, nil case nil: return acc, nil default: return nil, err } }