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"` ProfilingPassword string `toml:"profiling_password"`
// Allow registration on the server // Allow registration on the server
// If disabled, user must be manually created (currently via the debug 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 { type ConfigStorage struct {
@ -119,6 +121,10 @@ type ConfigExperimental struct {
// Both are created and stored for each local user. If this flag is enabled, // 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 // Linstrom shares the ED25519 key on request, otherwise the RSA key
UseEd25519Keys bool `toml:"use_ed25519_keys"` 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 { type Config struct {
@ -151,10 +157,12 @@ var defaultConfig Config = Config{
AdminMail: nil, AdminMail: nil,
}, },
Admin: ConfigAdmin{ Admin: ConfigAdmin{
Username: "server-admin", Username: "server-admin",
FirstTimeSetupOTP: "Example otp password", FirstTimeSetupOTP: "Example otp password",
ProfilingPassword: "Example profiling password", ProfilingPassword: "Example profiling password",
AllowRegistration: true, AllowRegistration: true,
AuthFetchForNonGet: true,
AuthFetchForGet: false,
}, },
Webauthn: ConfigWebAuthn{ Webauthn: ConfigWebAuthn{
DisplayName: "Linstrom", DisplayName: "Linstrom",
@ -198,7 +206,8 @@ var defaultConfig Config = Config{
UseSSL: false, UseSSL: false,
}, },
Experimental: ConfigExperimental{ Experimental: ConfigExperimental{
UseEd25519Keys: false, UseEd25519Keys: false,
AuthFetchForServerActor: false,
}, },
} }

View file

@ -11,6 +11,8 @@
first_time_setup_otp = "Example otp password" first_time_setup_otp = "Example otp password"
profiling_password = "" profiling_password = ""
allow_registration = true allow_registration = true
auth_fetch_for_get = false
auth_fetch_for_non_get = true
[webauthn] [webauthn]
display_name = "Linstrom" display_name = "Linstrom"
@ -47,3 +49,4 @@
[experimental] [experimental]
use_ed25519_keys = false use_ed25519_keys = false
auth_fetch_for_server_actor = false

10
main.go
View file

@ -4,6 +4,7 @@ package main
import ( import (
"embed" "embed"
"flag" "flag"
"fmt"
"path" "path"
"time" "time"
@ -163,6 +164,11 @@ func newServer() {
}() }()
} }
log.Info().Msg("Starting public server") log.Info().Msg("Starting public server")
public := webpublic.New(":8080", &defaultDuck) public := webpublic.New(
public.Start() 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] [general]
protocol = "https" protocol = "https"
domain = "serveo.net" domain = "serveo.net"
subdomain = "38e5543b0fc97680472709952a04622f" subdomain = "5dc9c90be02fecb3b132ad4d4877555a"
private_port = 8080 private_port = 8080
public_port = 443 public_port = 443
@ -13,6 +14,8 @@
first_time_setup_otp = "Example otp password" first_time_setup_otp = "Example otp password"
profiling_password = "" profiling_password = ""
allow_registration = true allow_registration = true
auth_fetch_for_get = false
auth_fetch_for_non_get = true
[webauthn] [webauthn]
display_name = "Linstrom" display_name = "Linstrom"
@ -49,3 +52,4 @@
[experimental] [experimental]
use_ed25519_keys = false use_ed25519_keys = false
auth_fetch_for_server_actor = false

View file

@ -6,6 +6,7 @@ import (
webutils "git.mstar.dev/mstar/goutils/http" webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/linstrom/config"
"git.mstar.dev/mstar/linstrom/web/public/api/activitypub" "git.mstar.dev/mstar/linstrom/web/public/api/activitypub"
webmiddleware "git.mstar.dev/mstar/linstrom/web/public/middleware" webmiddleware "git.mstar.dev/mstar/linstrom/web/public/middleware"
) )
@ -18,7 +19,10 @@ func BuildApiRouter() http.Handler {
"/activitypub", "/activitypub",
webutils.ChainMiddlewares( webutils.ChainMiddlewares(
activitypub.BuildActivitypubRouter(), 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/rsa"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/base64"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
"time"
webutils "git.mstar.dev/mstar/goutils/http" webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/other"
@ -17,12 +19,13 @@ import (
"git.mstar.dev/mstar/linstrom/activitypub" "git.mstar.dev/mstar/linstrom/activitypub"
"git.mstar.dev/mstar/linstrom/config" "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/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models" "git.mstar.dev/mstar/linstrom/storage-new/models"
webshared "git.mstar.dev/mstar/linstrom/web/shared" 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{ var publicPaths = []*regexp.Regexp{
regexp.MustCompile(`/\.well-known/.+^`), 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 // So either you get the server actor, which is intended behaviour, or access to
// one specific note that most likely won't even exist // one specific note that most likely won't even exist
// FIXME: Re-enable once implementation of actual verification is stable if !config.GlobalConfig.Experimental.AuthFetchForServerActor &&
// if strings.Contains(path, storage.ServerActorId) { strings.Contains(path, storage.ServerActorId) {
// log.Info().Msg("Server actor requested, no auth") log.Info().Msg("Server actor requested, no auth")
// h.ServeHTTP(w, r) h.ServeHTTP(w, r)
// return return
// } }
// Not an always open path, check methods // Not an always open path, check methods
if r.Method == "GET" && !forGet { if r.Method == "GET" && !forGet {
log.Info().Msg("Get request to AP resources don't need signature") 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 return
} }
log.Info().Msg("Need signature for AP request") 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") signatureHeader := r.Header.Get("Signature")
if signatureHeader == "" { if signatureHeader == "" {
log.Info().Msg("Received AP request without signature header where one is required") 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] rawAlgorithm1 := match[2]
rawHeaders := match[3] rawHeaders := match[3]
rawAlgorithm2 := match[4] 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 var rawAlgorithm string
if rawAlgorithm2 != "" { if rawAlgorithm2 != "" {
@ -127,7 +167,7 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
} else { } else {
rawAlgorithm = "hs2019" rawAlgorithm = "hs2019"
} }
_, err := url.Parse(rawKeyId) _, err = url.Parse(rawKeyId)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Key id is not an url") log.Warn().Err(err).Msg("Key id is not an url")
webutils.ProblemDetails( webutils.ProblemDetails(

View file

@ -3,11 +3,8 @@ package webshared
import ( import (
"io" "io"
"net/http" "net/http"
"strings"
"time" "time"
"git.mstar.dev/mstar/goutils/maputils"
"git.mstar.dev/mstar/linstrom/config" "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)) headers.Set("Date", time.Now().UTC().Format(http.TimeFormat))
} }
applyBodyHash(headers, postBody) 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 // 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 signedString string
var usedHeaders []string var usedHeaders []string
if config.GlobalConfig.Experimental.UseEd25519Keys { if config.GlobalConfig.Experimental.UseEd25519Keys {

View file

@ -6,7 +6,6 @@ import (
"net/url" "net/url"
"strings" "strings"
"git.mstar.dev/mstar/goutils/maputils"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/config"
@ -61,7 +60,10 @@ func genPreSignatureString(
headers http.Header, headers http.Header,
) (string, []string) { ) (string, []string) {
usedHeaders := []string{"(request-target)", "host"} 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 return GenerateStringToSign(method, target.Host, target.Path, headers, usedHeaders), usedHeaders
} }
@ -81,7 +83,7 @@ func GenerateStringToSign(
case "host": case "host":
dataBuilder.WriteString(v + ": " + host + "\n") dataBuilder.WriteString(v + ": " + host + "\n")
default: default:
dataBuilder.WriteString(v + ": " + headers.Get(v) + "\n") dataBuilder.WriteString(strings.ToLower(v) + ": " + headers.Get(v) + "\n")
} }
// dataBuilder.WriteString(k + ": " + v + "\n") // dataBuilder.WriteString(k + ": " + v + "\n")
// usedHeaders = append(usedHeaders, k) // usedHeaders = append(usedHeaders, k)