diff --git a/activitypub/import.go b/activitypub/import.go index 3e3d3f6..5e5edc5 100644 --- a/activitypub/import.go +++ b/activitypub/import.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" "time" @@ -52,7 +53,7 @@ type inboundImportUser struct { RestrictedFollow *bool `json:"manuallyApprovesFollowers"` } -func ImportRemoteAccount(targetName string) (string, error) { +func ImportRemoteAccountByHandle(targetName string) (string, error) { // Get the target user's link first webfinger, err := GetAccountWebfinger(targetName) if err != nil { @@ -65,177 +66,11 @@ func ImportRemoteAccount(targetName string) (string, error) { return "", errors.New("no self link") } APLink := selfLinks[0] - // Server actor key for signing - linstromActor, err := dbgen.User.Where(dbgen.User.Username.Eq(shared.ServerActorName)).First() + acc, err := ImportRemoteAccountByAPUrl(*APLink.Href) if err != nil { - return "", other.Error("activitypub", "failed to get server actor", err) + return "", err } - var response *http.Response - response, err = webshared.RequestSignedCavage("GET", *APLink.Href, nil, linstromActor) - if err != nil { - return "", other.Error("activitypub", "failed to complete cavage signed request", err) - } - defer response.Body.Close() - body, _ := io.ReadAll(response.Body) - log.Debug(). - Int("status", response.StatusCode). - Bytes("body", body). - Any("headers", response.Header). - Msg("Response information") - if response.StatusCode != 200 { - return "", fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode) - } - var data inboundImportUser - err = json.Unmarshal(body, &data) - if err != nil { - return "", other.Error("activitypub", "failed to unmarshal response", err) - } - log.Debug().Any("received-data", data).Msg("Response data") - - // TODO: Store received user in db - _, host, _ := SplitFullHandle(targetName) - hostId, err := ImportRemoteServer(host) - if err != nil { - return "", other.Error("activitypub", "failed to import host of target user", err) - } - - user, err := dbgen.User. - Where(dbgen.User.Username.Eq(targetName)). - Where(dbgen.User.ServerId.Eq(hostId)). - Preload(dbgen.User.RemoteInfo). - Preload(dbgen.User.InfoFields). - Preload(dbgen.User.BeingTypes). - Preload(dbgen.User.Roles). - FirstOrCreate() - if err != nil { - return "", other.Error("activitypub", "failed to find or create user in db", err) - } - user.Verified = true - user.FinishedRegistration = true - - if !sliceutils.ContainsFunc(user.Roles, func(t models.UserToRole) bool { - return t.Role.ID == models.DefaultUserRole.ID - }) { - roleMapping := models.UserToRole{ - Role: models.DefaultUserRole, - RoleId: models.DefaultUserRole.ID, - UserId: user.ID, - } - if err = dbgen.UserToRole.Create(&roleMapping); err != nil { - return "", other.Error( - "activitypub", - "failed to store default user role to imported account mapping", - err, - ) - } - if err = dbgen.User.Roles.Model(user).Append(&roleMapping); err != nil { - return "", other.Error("activitypub", "failed to attach default role to user", err) - } - } - if user.RemoteInfo == nil { - user.RemoteInfo = &models.UserRemoteLinks{ - UserId: user.ID, - ApLink: data.Id, - } - err = dbgen.UserRemoteLinks.Create(user.RemoteInfo) - if err != nil { - return "", other.Error("activitypub", "failed to create remote data for user", err) - } - err = dbgen.User.RemoteInfo.Model(user).Replace(user.RemoteInfo) - if err != nil { - return "", other.Error("activitypub", "failed to connect remote data to user", err) - } - user.RemoteInfoId.Int64 = int64(user.RemoteInfo.ID) - user.RemoteInfoId.Valid = true - } - if data.DisplayName != nil { - user.DisplayName = *data.DisplayName - } - if data.Outbox != nil { - user.RemoteInfo.OutboxLink.String = *data.Outbox - user.RemoteInfo.OutboxLink.Valid = true - } - user.RemoteInfo.InboxLink = data.Inbox - if data.PublicKey != nil { - pemBlock, _ := pem.Decode([]byte(data.PublicKey.Pem)) - if pemBlock.Type != "PUBLIC KEY" && pemBlock.Type != "RSA PUBLIC KEY" { - return "", fmt.Errorf("activitypub: invalid public key block type: %v", pemBlock.Type) - } - user.PublicKeyRsa = pemBlock.Bytes - } - // Assume published day of user won't change - if data.Description != nil { - user.Description = *data.Description - } - if data.PublicUrl != nil { - user.RemoteInfo.ViewLink.String = *data.PublicUrl - user.RemoteInfo.ViewLink.Valid = true - } - if data.Discoverable != nil { - user.Indexable = *data.Discoverable - } else { - // Assume false per default - user.Indexable = false - } - if data.Location != nil { - user.Location.String = *data.Location - user.Location.Valid = true - } - if data.Birthday != nil { - user.Birthday.String = *data.Birthday - user.Birthday.Valid = true - } - if data.RestrictedFollow != nil { - user.RestrictedFollow = *data.RestrictedFollow - } else { - // Assume not restricted if not included in received data - user.RestrictedFollow = false - } - if data.IsCat { - if !sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { - return t.Being == string(models.BEING_CAT) - }) { - log.Debug().Msg("user doesn't contain cat yet") - bt := models.UserToBeing{UserId: user.ID, Being: string(models.BEING_CAT)} - if err = dbgen.UserToBeing.Create(&bt); err != nil { - log.Warn().Err(err).Msg("Failed to append cat being type to imported user") - } - if err = dbgen.User.BeingTypes.Model(user).Append(&bt); err != nil { - log.Warn(). - Err(err). - Msg("Failed to append cat being type to imported user relations") - } - user.BeingTypes = append(user.BeingTypes, bt) - } - } else { - if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { - return t.Being == string(models.BEING_CAT) - }) { - _, err = dbgen.UserToBeing.Where(dbgen.UserToBeing.UserId.Eq(user.ID)).Where(dbgen.UserToBeing.Being.Eq(string(models.BEING_CAT))).Delete() - if err != nil { - log.Warn().Err(err).Msg("Failed to remove cat being type from user") - } - } - } - // Don't handle SpeakAsCat yet, as not included in stored data yet - - // Handle media last as more complicated - // Icon *inboundImportUserMedia `json:"icon,omitempty"` - // Banner *inboundImportUserMedia `json:"image,omitempty"` - - id := user.ID - user.ID = "" - if _, err = dbgen.User.Where(dbgen.User.ID.Eq(id)).UpdateColumns(user); err != nil { - return "", other.Error("activitypub", "failed to update imported user", err) - } - rlid := user.RemoteInfo.ID - user.RemoteInfo.ID = 0 - if _, err = dbgen.UserRemoteLinks.Where(dbgen.UserRemoteLinks.ID.Eq(rlid)).UpdateColumns(user.RemoteInfo); err != nil { - return "", other.Error("activitypub", "failed to update imported user's remote links", err) - } - - log.Info().Str("user", targetName).Msg("Import completed") - return id, nil + return acc.ID, nil } func ImportRemoteServer(host string) (uint, error) { @@ -282,7 +117,7 @@ func ImportRemoteServer(host string) (uint, error) { if err != nil { return 0, other.Error("activitypub", "failed to unmarshal info", err) } - log.Debug().Any("nodeinfo", data).Msg("Server info received") + // log.Debug().Any("nodeinfo", data).Msg("Server info received") rs := dbgen.RemoteServer // rsm := dbgen.RemoteServerMetadata serverModelType := webshared.MapNodeServerTypeToModelType(data.Software.Name) @@ -571,3 +406,181 @@ func ImportRemoteServer(host string) (uint, error) { } return id, nil } + +func ImportRemoteAccountByAPUrl(apUrl string) (*models.User, error) { + log.Info().Str("ap-url", apUrl).Msg("Importing account by ap url") + // Server actor key for signing + linstromActor, err := dbgen.User.Where(dbgen.User.Username.Eq(shared.ServerActorName)).First() + if err != nil { + return nil, other.Error("activitypub", "failed to get server actor", err) + } + var response *http.Response + response, err = webshared.RequestSignedCavage("GET", apUrl, nil, linstromActor) + if err != nil { + return nil, other.Error("activitypub", "failed to complete cavage signed request", err) + } + defer response.Body.Close() + body, _ := io.ReadAll(response.Body) + log.Debug(). + Int("status", response.StatusCode). + Bytes("body", body). + // Any("headers", response.Header). + Msg("Response information") + if response.StatusCode != 200 { + return nil, fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode) + } + var data inboundImportUser + err = json.Unmarshal(body, &data) + if err != nil { + return nil, other.Error("activitypub", "failed to unmarshal response", err) + } + // log.Debug().Any("received-data", data).Msg("Response data") + + targetUrl, err := url.Parse(apUrl) + if err != nil { + return nil, other.Error("activitypub", "failed to parse url as url", err) + } + hostId, err := ImportRemoteServer(targetUrl.Host) + if err != nil { + return nil, other.Error("activitypub", "failed to import host of target user", err) + } + + user, err := dbgen.User. + Where(dbgen.User.Username.Eq(data.PreferredUsername + "@" + targetUrl.Host)). + Where(dbgen.User.ServerId.Eq(hostId)). + Preload(dbgen.User.RemoteInfo). + Preload(dbgen.User.InfoFields). + Preload(dbgen.User.BeingTypes). + Preload(dbgen.User.Roles). + FirstOrCreate() + if err != nil { + return nil, other.Error("activitypub", "failed to find or create user in db", err) + } + user.Verified = true + user.FinishedRegistration = true + + if !sliceutils.ContainsFunc(user.Roles, func(t models.UserToRole) bool { + return t.Role.ID == models.DefaultUserRole.ID + }) { + roleMapping := models.UserToRole{ + Role: models.DefaultUserRole, + RoleId: models.DefaultUserRole.ID, + UserId: user.ID, + } + if err = dbgen.UserToRole.Create(&roleMapping); err != nil { + return nil, other.Error( + "activitypub", + "failed to store default user role to imported account mapping", + err, + ) + } + if err = dbgen.User.Roles.Model(user).Append(&roleMapping); err != nil { + return nil, other.Error("activitypub", "failed to attach default role to user", err) + } + } + if user.RemoteInfo == nil { + user.RemoteInfo = &models.UserRemoteLinks{ + UserId: user.ID, + ApLink: data.Id, + } + err = dbgen.UserRemoteLinks.Create(user.RemoteInfo) + if err != nil { + return nil, other.Error("activitypub", "failed to create remote data for user", err) + } + err = dbgen.User.RemoteInfo.Model(user).Replace(user.RemoteInfo) + if err != nil { + return nil, other.Error("activitypub", "failed to connect remote data to user", err) + } + user.RemoteInfoId.Int64 = int64(user.RemoteInfo.ID) + user.RemoteInfoId.Valid = true + } + if data.DisplayName != nil { + user.DisplayName = *data.DisplayName + } + if data.Outbox != nil { + user.RemoteInfo.OutboxLink.String = *data.Outbox + user.RemoteInfo.OutboxLink.Valid = true + } + user.RemoteInfo.InboxLink = data.Inbox + if data.PublicKey != nil { + pemBlock, _ := pem.Decode([]byte(data.PublicKey.Pem)) + if pemBlock.Type != "PUBLIC KEY" && pemBlock.Type != "RSA PUBLIC KEY" { + return nil, fmt.Errorf("activitypub: invalid public key block type: %v", pemBlock.Type) + } + user.PublicKeyRsa = pemBlock.Bytes + } + // Assume published day of user won't change + if data.Description != nil { + user.Description = *data.Description + } + if data.PublicUrl != nil { + user.RemoteInfo.ViewLink.String = *data.PublicUrl + user.RemoteInfo.ViewLink.Valid = true + } + if data.Discoverable != nil { + user.Indexable = *data.Discoverable + } else { + // Assume false per default + user.Indexable = false + } + if data.Location != nil { + user.Location.String = *data.Location + user.Location.Valid = true + } + if data.Birthday != nil { + user.Birthday.String = *data.Birthday + user.Birthday.Valid = true + } + if data.RestrictedFollow != nil { + user.RestrictedFollow = *data.RestrictedFollow + } else { + // Assume not restricted if not included in received data + user.RestrictedFollow = false + } + if data.IsCat { + if !sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { + return t.Being == string(models.BEING_CAT) + }) { + log.Debug().Msg("user doesn't contain cat yet") + bt := models.UserToBeing{UserId: user.ID, Being: string(models.BEING_CAT)} + if err = dbgen.UserToBeing.Create(&bt); err != nil { + log.Warn().Err(err).Msg("Failed to append cat being type to imported user") + } + if err = dbgen.User.BeingTypes.Model(user).Append(&bt); err != nil { + log.Warn(). + Err(err). + Msg("Failed to append cat being type to imported user relations") + } + user.BeingTypes = append(user.BeingTypes, bt) + } + } else { + if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { + return t.Being == string(models.BEING_CAT) + }) { + _, err = dbgen.UserToBeing.Where(dbgen.UserToBeing.UserId.Eq(user.ID)).Where(dbgen.UserToBeing.Being.Eq(string(models.BEING_CAT))).Delete() + if err != nil { + log.Warn().Err(err).Msg("Failed to remove cat being type from user") + } + } + } + // Don't handle SpeakAsCat yet, as not included in stored data yet + + // Handle media last as more complicated + // Icon *inboundImportUserMedia `json:"icon,omitempty"` + // Banner *inboundImportUserMedia `json:"image,omitempty"` + + id := user.ID + user.ID = "" + if _, err = dbgen.User.Where(dbgen.User.ID.Eq(id)).UpdateColumns(user); err != nil { + return nil, other.Error("activitypub", "failed to update imported user", err) + } + rlid := user.RemoteInfo.ID + user.RemoteInfo.ID = 0 + if _, err = dbgen.UserRemoteLinks.Where(dbgen.UserRemoteLinks.ID.Eq(rlid)).UpdateColumns(user.RemoteInfo); err != nil { + return nil, other.Error("activitypub", "failed to update imported user's remote links", err) + } + user.ID = id + + log.Info().Str("ap-url", apUrl).Msg("Import completed") + return user, nil +} diff --git a/main.go b/main.go index 8d2da46..001564f 100644 --- a/main.go +++ b/main.go @@ -135,6 +135,7 @@ func oldServer() { } func newServer() { + log.Info().Msg("Connectin to db") db, err := gorm.Open( postgres.Open(config.GlobalConfig.Storage.BuildPostgresDSN()), &gorm.Config{ @@ -145,9 +146,11 @@ func newServer() { log.Fatal().Err(err).Msg("Failed to start db") } dbgen.SetDefault(db) + log.Info().Msg("Applying migrations") if err = storagenew.Migrate(db); err != nil { log.Fatal().Err(err).Msg("Failed to automigrate structure") } + log.Info().Msg("Inserting self into db") if err = storagenew.InsertSelf(); err != nil { log.Fatal().Err(err).Msg("Failed to insert self properly") } diff --git a/storage-new/dbgen/users.gen.go b/storage-new/dbgen/users.gen.go index 5c5a9a2..a0dceab 100644 --- a/storage-new/dbgen/users.gen.go +++ b/storage-new/dbgen/users.gen.go @@ -1505,6 +1505,7 @@ type IUserDo interface { GetPagedAllDeleted(pageNr uint) (result []models.User, err error) GetPagedAllNonDeleted(pageNr uint) (result []models.User, err error) GdprUsers() (err error) + GetRemoteAccountByApUrl(url string) (result *models.User, err error) } // Get a user by a username, ignoring all restrictions on that user @@ -1636,6 +1637,30 @@ func (u userDo) GdprUsers() (err error) { return } +// Get user by AP url +// +// SELECT * FROM @@table +// +// WHERE id in ( +// SELECT user_id FROM user_remote_links +// WHERE ap_link = @url LIMIT 1 +// ) +// +// LIMIT 1 +func (u userDo) GetRemoteAccountByApUrl(url string) (result *models.User, err error) { + var params []interface{} + + var generateSQL strings.Builder + params = append(params, url) + generateSQL.WriteString("SELECT * FROM users WHERE id in ( SELECT user_id FROM user_remote_links WHERE ap_link = ? LIMIT 1 ) LIMIT 1 ") + + var executeSQL *gorm.DB + executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Take(&result) // ignore_security_alert + err = executeSQL.Error + + return +} + func (u userDo) Debug() IUserDo { return u.withDO(u.DO.Debug()) } diff --git a/storage-new/models/User.go b/storage-new/models/User.go index befa840..e476a83 100644 --- a/storage-new/models/User.go +++ b/storage-new/models/User.go @@ -136,4 +136,14 @@ type IUser interface { // // DELETE FROM @@table WHERE deleted_at IS NOT NULL AND deleted_at + interval '30 days' < NOW() GdprUsers() error + + // Get user by AP url + // + // SELECT * FROM @@table + // WHERE id in ( + // SELECT user_id FROM user_remote_links + // WHERE ap_link = @url LIMIT 1 + // ) + // LIMIT 1 + GetRemoteAccountByApUrl(url string) (*gen.T, error) } diff --git a/storage-new/self.go b/storage-new/self.go index 3383d2c..a62d3a7 100644 --- a/storage-new/self.go +++ b/storage-new/self.go @@ -14,6 +14,10 @@ import ( "git.mstar.dev/mstar/linstrom/storage-new/models" ) +// ID of the server actor account in the db. +// Set by InsertSelf +var ServerActorId = "" + func InsertSelf() error { if err := insertRoles(); err != nil { return other.Error("storage", "failed to save/update default roles", err) @@ -30,6 +34,7 @@ func InsertSelf() error { if err != nil { return other.Error("storage", "failed to save/update self user", err) } + ServerActorId = user.ID if err = insertUserPronoun(user); err != nil { return other.Error("storage", "failed to save/update self user pronoun", err) } diff --git a/temp.toml b/temp.toml index 8efde9d..b3cf406 100644 --- a/temp.toml +++ b/temp.toml @@ -1,7 +1,7 @@ [general] protocol = "https" - domain = "lhr.life" - subdomain = "3e4af10addc7d0" + domain = "serveo.net" + subdomain = "38e5543b0fc97680472709952a04622f" private_port = 8080 public_port = 443 diff --git a/web/debug/users.go b/web/debug/users.go index c013c75..040e74b 100644 --- a/web/debug/users.go +++ b/web/debug/users.go @@ -205,7 +205,7 @@ func returnKeypair(w http.ResponseWriter, r *http.Request) { func issueUserImport(w http.ResponseWriter, r *http.Request) { target := r.FormValue("target") - _, err := activitypub.ImportRemoteAccount(target) + _, err := activitypub.ImportRemoteAccountByHandle(target) hlog.FromRequest(r).Info().Err(err).Msg("Err from import request") } diff --git a/web/public/api/api.go b/web/public/api/api.go index 9340392..3570a43 100644 --- a/web/public/api/api.go +++ b/web/public/api/api.go @@ -4,14 +4,23 @@ import ( "fmt" "net/http" + webutils "git.mstar.dev/mstar/goutils/http" + "git.mstar.dev/mstar/linstrom/web/public/api/activitypub" + webmiddleware "git.mstar.dev/mstar/linstrom/web/public/middleware" ) func BuildApiRouter() http.Handler { router := http.NewServeMux() router.Handle( "/activitypub/", - http.StripPrefix("/activitypub", activitypub.BuildActivitypubRouter()), + http.StripPrefix( + "/activitypub", + webutils.ChainMiddlewares( + activitypub.BuildActivitypubRouter(), + webmiddleware.BuildAuthorizedFetchCheck(true, true), + ), + ), ) router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "in api") diff --git a/web/public/middleware/authFetchCheck.go b/web/public/middleware/authFetchCheck.go index 7c7a0d3..485b05b 100644 --- a/web/public/middleware/authFetchCheck.go +++ b/web/public/middleware/authFetchCheck.go @@ -1,15 +1,28 @@ 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_\-\.:@/\?&=#%\+\[\]!$\(\)\*,;]+),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/.+^`), @@ -27,6 +40,9 @@ var signatureRegex = regexp.MustCompile(signatureRegexString) 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 { @@ -35,17 +51,36 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil 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 } - // TODO: Perform check here + 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, @@ -56,8 +91,14 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil ) 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, @@ -73,12 +114,22 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil } // fullMatch = match[0] rawKeyId := match[1] - rawHeaders := match[2] - rawAlgorithm := match[3] - signature := match[4] + 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, @@ -90,15 +141,111 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil return } - if rawAlgorithm == "" { - rawAlgorithm = "hs2019" - // w.Header().Add("X-Algorithm-Hint", "") - } - - _ = rawHeaders - _ = signature _ = rawAlgorithm - panic("not implemented") + 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 + } +} diff --git a/web/public/server.go b/web/public/server.go index 6dacc0b..aa56ee7 100644 --- a/web/public/server.go +++ b/web/public/server.go @@ -34,6 +34,7 @@ import ( webutils "git.mstar.dev/mstar/goutils/http" "git.mstar.dev/mstar/linstrom/web/public/api" + webmiddleware "git.mstar.dev/mstar/linstrom/web/public/middleware" ) type Server struct { @@ -61,6 +62,7 @@ func New(addr string, duckImg *string) *Server { Handler: webutils.ChainMiddlewares( handler, webutils.BuildLoggingMiddleware(map[string]string{"server": "public"}), + webmiddleware.AppendFullPathMiddleware, ), Addr: addr, } diff --git a/web/shared/client.go b/web/shared/client.go index 0cd770f..5ccd00a 100644 --- a/web/shared/client.go +++ b/web/shared/client.go @@ -43,14 +43,14 @@ func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte var signedString string var usedHeaders []string if config.GlobalConfig.Experimental.UseEd25519Keys { - tmp, tmp2, err := CreateSignatureED(method, r.URL, mappedHeaders, privateKeyBytes) + tmp, tmp2, err := CreateSignatureED(method, r.URL, headers, privateKeyBytes) if err != nil { return err } signedString = tmp usedHeaders = tmp2 } else { - tmp, tmp2, err := CreateSignatureRSA(method, r.URL, mappedHeaders, privateKeyBytes) + tmp, tmp2, err := CreateSignatureRSA(method, r.URL, headers, privateKeyBytes) if err != nil { return err } diff --git a/web/shared/signing.go b/web/shared/signing.go index b35a65b..e776ae6 100644 --- a/web/shared/signing.go +++ b/web/shared/signing.go @@ -2,9 +2,11 @@ package webshared import ( "encoding/base64" + "net/http" "net/url" "strings" + "git.mstar.dev/mstar/goutils/maputils" "github.com/rs/zerolog/log" "git.mstar.dev/mstar/linstrom/config" @@ -17,7 +19,7 @@ import ( func CreateSignatureRSA( method string, target *url.URL, - headers map[string]string, + headers http.Header, privateKeyBytes []byte, ) (string, []string, error) { message, usedHeaders := genPreSignatureString(method, target, headers) @@ -42,7 +44,7 @@ func CreateSignatureRSA( func CreateSignatureED( method string, target *url.URL, - headers map[string]string, + headers http.Header, privateKeyBytes []byte, ) (string, []string, error) { message, usedHeaders := genPreSignatureString(method, target, headers) @@ -56,24 +58,37 @@ func CreateSignatureED( func genPreSignatureString( method string, target *url.URL, - headers map[string]string, + headers http.Header, ) (string, []string) { - dataBuilder := strings.Builder{} - dataBuilder.WriteString("(request-target): ") - dataBuilder.WriteString(strings.ToLower(method) + " ") - dataBuilder.WriteString(target.Path + "\n") - dataBuilder.WriteString("host: ") - dataBuilder.WriteString(target.Host + "\n") - // dataBuilder.WriteString("algorithm: rsa-sha256\n") - // usedHeaders := []string{"(request-target)", "algorithm"} usedHeaders := []string{"(request-target)", "host"} - for k, v := range headers { - dataBuilder.WriteString(k + ": " + v + "\n") - usedHeaders = append(usedHeaders, k) + usedHeaders = append(usedHeaders, maputils.KeysFromMap(headers)...) + return GenerateStringToSign(method, target.Host, target.Path, headers, usedHeaders), usedHeaders +} + +func GenerateStringToSign( + method string, + host string, + path string, + headers http.Header, + headerOrder []string, +) string { + dataBuilder := strings.Builder{} + for _, v := range headerOrder { + v = strings.ToLower(v) + switch v { + case "(request-target)": + dataBuilder.WriteString(v + ": " + strings.ToLower(method) + " " + path + "\n") + case "host": + dataBuilder.WriteString(v + ": " + host + "\n") + default: + dataBuilder.WriteString(v + ": " + headers.Get(v) + "\n") + } + // dataBuilder.WriteString(k + ": " + v + "\n") + // usedHeaders = append(usedHeaders, k) } tmp := strings.TrimSuffix(dataBuilder.String(), "\n") log.Debug().Str("Raw signature string", tmp).Send() - return tmp, usedHeaders + return tmp } // Generate the content of the "Signature" header based on