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

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