Signing works

This commit is contained in:
Melody Becker 2025-04-10 16:40:06 +02:00
parent d272fa90b4
commit da2a89010c
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
19 changed files with 348 additions and 100 deletions

View file

@ -4,10 +4,9 @@ import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
httputils "git.mstar.dev/mstar/goutils/http"
webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/goutils/sliceutils"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
@ -27,15 +26,28 @@ func postAs(w http.ResponseWriter, r *http.Request) {
data := Inbound{}
err := dec.Decode(&data)
if err != nil {
httputils.HttpErr(w, 0, "json decode failed", http.StatusBadRequest)
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"bad request data",
nil,
map[string]any{
"sample": Inbound{
Username: "bob",
Content: "Heya there, this is sample data",
},
},
)
return
}
user, err := dbgen.User.GetByUsername(data.Username)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
httputils.HttpErr(w, 0, "no user with that name", http.StatusNotFound)
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
} else {
log.Error().Err(err).Str("name", data.Username).Msg("Failed to find user")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
}
return
}
@ -69,13 +81,13 @@ func notesFrom(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Error().Err(err).Str("name", username).Msg("Failed to get user")
storage.HandleReconnectError(err)
httputils.HttpErr(w, 0, "failed to get user", http.StatusInternalServerError)
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
notes, err := dbgen.Note.GetNotesPaged(user.ID, 0, uint8(models.NOTE_TARGET_PUBLIC))
if err != nil {
log.Error().Err(err).Str("name", username).Msg("Failed to get notes")
httputils.HttpErr(w, 0, "failed to get notes", http.StatusInternalServerError)
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
publicNotes := sliceutils.Map(notes, func(t models.Note) webshared.Note {
@ -83,11 +95,5 @@ func notesFrom(w http.ResponseWriter, r *http.Request) {
n.FromModel(&t)
return n
})
jsonNotes, err := json.Marshal(publicNotes)
if err != nil {
log.Error().Err(err).Msg("Failed to marshal notes")
httputils.HttpErr(w, 0, "failed to marshal", http.StatusInternalServerError)
return
}
fmt.Fprint(w, string(jsonNotes))
webutils.SendJson(w, publicNotes)
}

View file

@ -2,19 +2,18 @@ package webdebug
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
httputils "git.mstar.dev/mstar/goutils/http"
webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/goutils/other"
"git.mstar.dev/mstar/goutils/sliceutils"
"github.com/rs/zerolog/log"
"git.mstar.dev/mstar/linstrom/shared"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models"
webshared "git.mstar.dev/mstar/linstrom/web/shared"
@ -27,25 +26,27 @@ func getNonDeletedUsers(w http.ResponseWriter, r *http.Request) {
var err error
page, err = strconv.Atoi(pageStr)
if err != nil {
httputils.HttpErr(w, 0, "page is not a number", http.StatusBadRequest)
webutils.HttpErr(w, 0, "page is not a number", http.StatusBadRequest)
return
}
}
users, err := dbgen.User.GetPagedAllNonDeleted(uint(page))
if err != nil {
httputils.HttpErr(w, 0, "failed to get users", http.StatusInternalServerError)
webutils.ProblemDetails(
w,
http.StatusInternalServerError,
"/errors/db-failure",
"database failure",
nil,
nil,
)
return
}
marshalled, err := json.Marshal(sliceutils.Map(users, func(t models.User) webshared.User {
webutils.SendJson(w, sliceutils.Map(users, func(t models.User) webshared.User {
u := webshared.User{}
u.FromModel(&t)
return u
}))
if err != nil {
httputils.HttpErr(w, 0, "failed to marshal users", http.StatusInternalServerError)
return
}
fmt.Fprint(w, string(marshalled))
}
func createLocalUser(w http.ResponseWriter, r *http.Request) {
@ -61,19 +62,43 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
data := Inbound{}
err := jsonDecoder.Decode(&data)
if err != nil {
httputils.HttpErr(w, 0, "decode failed", http.StatusBadRequest)
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"bad request data",
nil,
map[string]any{
"sample": Inbound{
Username: "bob",
Displayname: "Bob Bobbington",
Description: "Bobbing Bobs bop to Bobs bobbing beats",
Birthday: other.IntoPointer(time.Now()),
Location: nil,
IsBot: false,
},
},
)
return
}
// publicKey, privateKey, err := ed25519.GenerateKey(nil)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
publicKeyEdBytes, privateKeyEdBytes, err := shared.GenerateKeypair(true)
if err != nil {
log.Error().Err(err).Msg("Failed to generate and marshal public key")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
publicKeyRsaBytes, privateKeyRsaBytes, err := shared.GenerateKeypair(false)
if err != nil {
log.Error().Err(err).Msg("Failed to generate and marshal public key")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
pkeyId := make([]byte, 64)
_, err = rand.Read(pkeyId)
if err != nil {
log.Error().Err(err).Msg("Failed to generate passkey id")
httputils.HttpErr(w, 0, "failed to generate passkey id", http.StatusInternalServerError)
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
@ -84,8 +109,10 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
u.Description,
u.IsBot,
u.ServerId,
u.PrivateKey,
u.PublicKey,
u.PrivateKeyEd,
u.PublicKeyEd,
u.PrivateKeyRsa,
u.PublicKeyRsa,
u.PasskeyId,
)
if data.Birthday != nil {
@ -95,14 +122,16 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
query = query.Select(u.Location)
}
user := models.User{
Username: data.Username,
DisplayName: data.Displayname,
Description: data.Description,
IsBot: data.IsBot,
ServerId: 1, // Hardcoded, Self is always first ID
PublicKey: publicKeyBytes,
PrivateKey: privateKeyBytes,
PasskeyId: pkeyId,
Username: data.Username,
DisplayName: data.Displayname,
Description: data.Description,
IsBot: data.IsBot,
ServerId: 1, // Hardcoded, Self is always first ID
PublicKeyRsa: publicKeyRsaBytes,
PublicKeyEd: publicKeyEdBytes,
PrivateKeyRsa: privateKeyRsaBytes,
PrivateKeyEd: privateKeyEdBytes,
PasskeyId: pkeyId,
}
if data.Birthday != nil {
user.Birthday = sql.NullTime{Valid: true, Time: *data.Birthday}
@ -112,7 +141,7 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
}
if err = u.Create(&user); err != nil {
log.Error().Err(err).Msg("failed to create new local user")
httputils.HttpErr(w, 0, "db failure", http.StatusInternalServerError)
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
}
}

View file

@ -8,6 +8,7 @@ import (
func BuildActivitypubRouter() http.Handler {
router := http.NewServeMux()
router.HandleFunc("/user/{id}", users)
router.HandleFunc("/user/{id}/inbox", userInbox)
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "in ap")
})

View file

@ -3,13 +3,17 @@ package activitypub
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
webutils "git.mstar.dev/mstar/goutils/http"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/hlog"
"git.mstar.dev/mstar/linstrom/config"
"git.mstar.dev/mstar/linstrom/storage-new"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
webshared "git.mstar.dev/mstar/linstrom/web/shared"
)
var baseLdContext = []any{
@ -23,17 +27,30 @@ func users(w http.ResponseWriter, r *http.Request) {
Owner string `json:"owner"`
Pem string `json:"publicKeyPem"`
}
type Outbound struct {
Context []any `json:"@context"`
Id string `json:"id"`
Type string `json:"type"`
PreferredUsername string `json:"preferredUsername"`
Inbox string `json:"inbox"`
// FIXME: Public key stuff is borken. Focus on fixing
// PublicKey OutboundKey `json:"publicKey"`
type OutboundMedia struct {
Type string `json:"type"`
Url string `json:"url"`
MediaType string `json:"mediaType"`
}
type Outbound struct {
Context []any `json:"@context"`
Id string `json:"id"`
Type string `json:"type"`
PreferredUsername string `json:"preferredUsername"`
Inbox string `json:"inbox"`
PublicKey OutboundKey `json:"publicKey"`
Published time.Time `json:"published"`
DisplayName string `json:"name"`
Description *string `json:"summary,omitempty"`
PublicUrl string `json:"url"`
Icon *OutboundMedia `json:"icon,omitempty"`
Banner *OutboundMedia `json:"image,omitempty"`
}
log := hlog.FromRequest(r)
userId := r.PathValue("id")
user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).First()
user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).
Preload(dbgen.User.Icon).Preload(dbgen.User.Banner).
First()
if err != nil {
webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil)
if storage.HandleReconnectError(err) {
@ -43,20 +60,47 @@ func users(w http.ResponseWriter, r *http.Request) {
}
return
}
// fmt.Println(x509.ParsePKCS1PublicKey(user.PublicKey))
apUrl := userIdToApUrl(user.ID)
var keyBytes string
if config.GlobalConfig.Experimental.UseEd25519Keys {
keyBytes = keyBytesToPem(user.PublicKeyEd)
} else {
keyBytes = keyBytesToPem(user.PublicKeyRsa)
}
data := Outbound{
Context: baseLdContext,
Id: apUrl,
Type: "Person",
PreferredUsername: user.DisplayName,
PreferredUsername: user.Username,
Inbox: apUrl + "/inbox",
// PublicKey: OutboundKey{
// Id: apUrl + "#main-key",
// Owner: apUrl,
// Pem: keyBytesToPem(user.PublicKey),
// },
PublicKey: OutboundKey{
Id: apUrl + "#main-key",
Owner: apUrl,
Pem: keyBytes,
},
Published: user.CreatedAt,
DisplayName: user.DisplayName,
PublicUrl: config.GlobalConfig.General.GetFullPublicUrl() + "/user/" + user.Username,
}
if user.Description != "" {
data.Description = &user.Description
}
if user.Icon != nil {
log.Debug().Msg("icon found")
data.Icon = &OutboundMedia{
Type: "Image",
Url: webshared.EnsurePublicUrl(user.Icon.Location),
MediaType: user.Icon.Type,
}
}
if user.Banner != nil {
log.Debug().Msg("icon banner")
data.Banner = &OutboundMedia{
Type: "Image",
Url: webshared.EnsurePublicUrl(user.Banner.Location),
MediaType: user.Banner.Type,
}
}
encoded, err := json.Marshal(data)
@ -64,6 +108,13 @@ func users(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(encoded))
}
func userInbox(w http.ResponseWriter, r *http.Request) {
log := hlog.FromRequest(r)
userId := r.PathValue("id")
data, err := io.ReadAll(r.Body)
log.Info().Err(err).Str("userId", userId).Bytes("body", data).Msg("Inbox message")
}
/*
Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then
Fuck you.

View file

@ -10,6 +10,7 @@ import (
var errorDescriptions = map[string]string{
"webfinger-bad-resource": "The given format for the \"resource\" url parameter was missing or invalid. It must follow the form \"acct:<username>@<domain>\"",
"db-failure": "The database query for this request failed for an undisclosed reason. This is often caused by bad input data conflicting with existing information. Try to submit different data or wait for some time",
"bad-request-data": "The data provided in the request doesn't match the requirements, see problem details' detail field for more information",
}
func errorTypeHandler(w http.ResponseWriter, r *http.Request) {

View file

@ -40,7 +40,7 @@ type Server struct {
server *http.Server
}
func New(addr string) *Server {
func New(addr string, duckImg *string) *Server {
handler := http.NewServeMux()
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
@ -51,6 +51,11 @@ func New(addr string) *Server {
handler.HandleFunc("GET /.well-known/nodeinfo", api.WellKnownNodeinfo)
handler.HandleFunc("GET /nodeinfo/2.1", api.Nodeinfo)
handler.HandleFunc("GET /errors/{name}", errorTypeHandler)
handler.HandleFunc("GET /default-image", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "image/web")
w.Header().Add("Content-Disposition", "attachment; filename=\"duck.webp\"")
fmt.Fprint(w, *duckImg)
})
server := http.Server{
Handler: webutils.ChainMiddlewares(handler, webutils.LoggingMiddleware),
Addr: addr,

View file

@ -4,6 +4,7 @@ import (
"slices"
"time"
"git.mstar.dev/mstar/linstrom/config"
"git.mstar.dev/mstar/linstrom/shared"
"git.mstar.dev/mstar/linstrom/storage-new/models"
)
@ -97,7 +98,11 @@ func (u *User) FromModel(m *models.User) {
u.BannerId = &m.IconId.String
}
u.Indexable = m.Indexable
u.PublicKey = append(u.PublicKey, m.PublicKey...)
if config.GlobalConfig.Experimental.UseEd25519Keys {
u.PublicKey = append(u.PublicKey, m.PublicKeyEd...)
} else {
u.PublicKey = append(u.PublicKey, m.PublicKeyRsa...)
}
u.RestrictedFollow = m.RestrictedFollow
if m.Location.Valid {
u.Location = &m.Location.String

View file

@ -0,0 +1,16 @@
package webshared
import "strings"
// TODO: Define linstrom uri type
var hardcodedUrls = map[string]string{
"default-media": "/default-image",
}
func EnsurePublicUrl(rawUrl string) string {
if !strings.HasPrefix(rawUrl, "linstrom://") {
return rawUrl
}
return strings.Replace(rawUrl, "linstrom://", "/", 1)
}

40
web/shared/signing.go Normal file
View file

@ -0,0 +1,40 @@
package webshared
import (
"strings"
"git.mstar.dev/mstar/linstrom/shared"
)
func CreateSignatureRSA(
method string,
target string,
headers map[string]string,
privateKeyBytes []byte,
) (string, error) {
message := genPreSignatureString(method, target, headers)
signed, err := shared.Sign(message, privateKeyBytes, true)
return string(signed), err
}
func CreateSignatureED(
method string,
target string,
headers map[string]string,
privateKeyBytes []byte,
) (string, error) {
message := genPreSignatureString(method, target, headers)
signed, err := shared.Sign(message, privateKeyBytes, false)
return string(signed), err
}
func genPreSignatureString(method, target string, headers map[string]string) string {
dataBuilder := strings.Builder{}
dataBuilder.WriteString("(request-target) ")
dataBuilder.WriteString(strings.ToLower(method) + " ")
dataBuilder.WriteString(target + "\n")
for k, v := range headers {
dataBuilder.WriteString(k + ": " + v + "\n")
}
return dataBuilder.String()
}