Auth fetch verification (cavage) works now
All checks were successful
/ docker (push) Successful in 4m14s

- Verifying inbound requests signed with Cavage are now checked as
  expected
- Fixed a bug where the signature header is not generated correctly
- Extended config to include settings for what requests to verify
- Fixed new server in main not using internal port from config
This commit is contained in:
Melody Becker 2025-04-22 15:27:24 +02:00
parent 271acc8d29
commit 627926460c
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
8 changed files with 90 additions and 36 deletions

View file

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

View file

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

10
main.go
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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