More work on getting auth fetch verification working

This commit is contained in:
Melody Becker 2025-04-20 22:10:35 +02:00
parent 7eac1db475
commit 9957ba8302
12 changed files with 434 additions and 205 deletions

View file

@ -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
}

View file

@ -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")
}

View file

@ -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())
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -1,7 +1,7 @@
[general]
protocol = "https"
domain = "lhr.life"
subdomain = "3e4af10addc7d0"
domain = "serveo.net"
subdomain = "38e5543b0fc97680472709952a04622f"
private_port = 8080
public_port = 443

View file

@ -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")
}

View file

@ -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")

View file

@ -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
}
}

View file

@ -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,
}

View file

@ -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
}

View file

@ -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