IT FUCKING WORKS
All checks were successful
/ docker (push) Successful in 4m9s

(except for iceshrimp, but who cares) (well, I do. Would not be nice to
not be compatible with a not-so-rarely used software)
This commit is contained in:
Melody Becker 2025-04-14 23:14:30 +02:00
parent 59dd8d82cf
commit 5e13817563
No known key found for this signature in database
7 changed files with 167 additions and 93 deletions

View file

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"net/http"
"time" "time"
"git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/other"
@ -12,6 +13,7 @@ import (
"git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/config"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models"
webshared "git.mstar.dev/mstar/linstrom/web/shared" webshared "git.mstar.dev/mstar/linstrom/web/shared"
) )
@ -54,35 +56,35 @@ func ImportRemoteAccount(targetName string) (string, error) {
return t.Relation == "self" return t.Relation == "self"
}) })
if len(selfLinks) == 0 { if len(selfLinks) == 0 {
return "", errors.New("No self link") return "", errors.New("no self link")
} }
APLink := selfLinks[0] APLink := selfLinks[0]
req, err := webshared.NewRequest("GET", *APLink.Href, nil)
if err != nil {
return "", err
}
req.Header.Add("Accept", "application/activity+json")
// Server actor key for signing // Server actor key for signing
linstromActor, err := dbgen.User.Where(dbgen.User.Username.Eq("linstrom")).First() linstromActor, err := dbgen.User.Where(dbgen.User.Username.Eq("linstrom")).First()
if err != nil { if err != nil {
return "", err return "", err
} }
var keyBytes []byte var response *http.Response
if config.GlobalConfig.Experimental.UseEd25519Keys { // if config.GlobalConfig.Experimental.UseEd25519Keys {
keyBytes = linstromActor.PrivateKeyEd // response, err = webshared.RequestSignedCavage(
} else { // "GET",
keyBytes = linstromActor.PrivateKeyRsa // *APLink.Href,
} // nil,
// config.GlobalConfig.General.GetFullPublicUrl()+"/api/activitypub/user/"+linstromActor.ID+"#main-key",
// Sign and send // linstromActor.PrivateKeyEd,
err = webshared.SignRequest(req, linstromActor.ID+"#main-key", keyBytes, nil) // true,
// err = webshared.SignRequestWithHttpsig(req, linstromActor.ID+"#main-key", keyBytes, nil) // )
if err != nil { // } else {
return "", err // response, err = webshared.RequestSignedCavage(
} // "GET",
log.Debug().Str("Signature", req.Header.Get("Signature")).Msg("Post sign signature") // *APLink.Href,
response, err := webshared.RequestClient.Do(req) // nil,
// config.GlobalConfig.General.GetFullPublicUrl()+"/api/activitypub/user/"+linstromActor.ID+"#main-key",
// linstromActor.PrivateKeyRsa,
// false,
// )
// }
response, err = customReq(*APLink.Href, linstromActor)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -94,7 +96,7 @@ func ImportRemoteAccount(targetName string) (string, error) {
Any("headers", response.Header). Any("headers", response.Header).
Msg("Response information") Msg("Response information")
if response.StatusCode != 200 { if response.StatusCode != 200 {
return "", errors.New("Bad status") return "", errors.New("bad status")
} }
var data InboundUser var data InboundUser
err = json.Unmarshal(body, &data) err = json.Unmarshal(body, &data)
@ -104,3 +106,26 @@ func ImportRemoteAccount(targetName string) (string, error) {
log.Info().Any("received-data", data).Msg("Response data") log.Info().Any("received-data", data).Msg("Response data")
return "", nil return "", nil
} }
func customReq(target string, linstromActor *models.User) (*http.Response, error) {
req, err := webshared.NewRequest("GET", target, nil)
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/activity+json")
var keyBytes []byte
if config.GlobalConfig.Experimental.UseEd25519Keys {
keyBytes = linstromActor.PrivateKeyEd
} else {
keyBytes = linstromActor.PrivateKeyRsa
}
// Sign and send
err = webshared.SignRequest(req, linstromActor.ID+"#main-key", keyBytes, nil)
// err = webshared.SignRequestWithHttpsig(req, linstromActor.ID+"#main-key", keyBytes, nil)
if err != nil {
return nil, err
}
return webshared.RequestClient.Do(req)
}

1
go.mod
View file

@ -47,6 +47,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.7.1 // indirect github.com/ebitengine/purego v0.7.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-webauthn/x v0.1.14 // indirect github.com/go-webauthn/x v0.1.14 // indirect

2
go.sum
View file

@ -103,6 +103,8 @@ github.com/gen2brain/avif v0.3.2 h1:XUR0CBl5n4ISFJE8/pc1RMEKt5KUVoW8InctN+M7+DQ=
github.com/gen2brain/avif v0.3.2/go.mod h1:tdL2sV6oOJXBZZvT5iP55VEM1X2c3/yJmYKMJTl8fXg= github.com/gen2brain/avif v0.3.2/go.mod h1:tdL2sV6oOJXBZZvT5iP55VEM1X2c3/yJmYKMJTl8fXg=
github.com/go-ap/httpsig v0.0.0-20221203064646-3647b4d88fdf h1:Ab5yBsD/dXhFmgf2hX7T/YYr+VK0Df7SrIxyNztT9YE= github.com/go-ap/httpsig v0.0.0-20221203064646-3647b4d88fdf h1:Ab5yBsD/dXhFmgf2hX7T/YYr+VK0Df7SrIxyNztT9YE=
github.com/go-ap/httpsig v0.0.0-20221203064646-3647b4d88fdf/go.mod h1:+4SUDMvPlRMUPW5PlMTbxj3U5a4fWasBIbakUw7Kp6c= github.com/go-ap/httpsig v0.0.0-20221203064646-3647b4d88fdf/go.mod h1:+4SUDMvPlRMUPW5PlMTbxj3U5a4fWasBIbakUw7Kp6c=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=

View file

@ -1,7 +1,7 @@
[general] [general]
protocol = "https" protocol = "https"
domain = "serveo.net" domain = "serveo.net"
subdomain = "29a62d7d1dd05bd000b8f9138244df2c" subdomain = "cc45720a387f04ba6a748a2627327a77"
private_port = 8080 private_port = 8080
public_port = 443 public_port = 443

View file

@ -1,18 +1,12 @@
package webshared package webshared
import ( import (
"crypto/ed25519"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"git.mstar.dev/mstar/goutils/maputils" "git.mstar.dev/mstar/goutils/maputils"
"github.com/go-ap/httpsig"
"github.com/rs/zerolog/log"
"git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/config"
) )
@ -21,8 +15,6 @@ import (
var RequestClient = http.Client{} var RequestClient = http.Client{}
const xRandomHeader = "X-Auth-Random"
// Sign a given outbound request for authorized fetch. // Sign a given outbound request for authorized fetch.
// At the end, the Signature header will have the signature needed, // At the end, the Signature header will have the signature needed,
// nothing else is modified. // nothing else is modified.
@ -31,19 +23,9 @@ const xRandomHeader = "X-Auth-Random"
func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte) error { func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte) error {
method := r.Method method := r.Method
headers := r.Header headers := r.Header
var nowString string
if dateString := headers.Get("Date"); dateString != "" { if dateString := headers.Get("Date"); dateString != "" {
nowString = dateString
} else { } else {
nowString = time.Now().Format("Mon, 02 Jan 2006 15:04:05 MST") headers.Set("Date", time.Now().UTC().Format(http.TimeFormat))
headers.Set("Date", nowString)
}
var host string
if hostString := headers.Get("Host"); hostString != "" {
host = hostString
} else {
host = config.GlobalConfig.General.GetFullDomain()
headers.Set("Host", host)
} }
applyBodyHash(headers, postBody) applyBodyHash(headers, postBody)
mappedHeaders := maputils.MapNewKeys(headers, func(k string, v []string) (string, string) { mappedHeaders := maputils.MapNewKeys(headers, func(k string, v []string) (string, string) {
@ -61,14 +43,14 @@ func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte
var signedString string var signedString string
var usedHeaders []string var usedHeaders []string
if config.GlobalConfig.Experimental.UseEd25519Keys { if config.GlobalConfig.Experimental.UseEd25519Keys {
tmp, tmp2, err := CreateSignatureED(method, r.URL.Path, mappedHeaders, privateKeyBytes) tmp, tmp2, err := CreateSignatureED(method, r.URL, mappedHeaders, privateKeyBytes)
if err != nil { if err != nil {
return err return err
} }
signedString = tmp signedString = tmp
usedHeaders = tmp2 usedHeaders = tmp2
} else { } else {
tmp, tmp2, err := CreateSignatureRSA(method, r.URL.Path, mappedHeaders, privateKeyBytes) tmp, tmp2, err := CreateSignatureRSA(method, r.URL, mappedHeaders, privateKeyBytes)
if err != nil { if err != nil {
return err return err
} }
@ -80,47 +62,10 @@ func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte
signedString, signedString,
usedHeaders..., usedHeaders...,
) )
log.Debug().Str("signature-header", signature).Send()
headers.Set("Signature", signature) headers.Set("Signature", signature)
return nil return nil
} }
func SignRequestWithHttpsig(
r *http.Request,
keyId string,
privateKeyBytes, postBody []byte,
) error {
keyId = config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/user/" + keyId
if config.GlobalConfig.Experimental.UseEd25519Keys {
key := ed25519.PrivateKey(privateKeyBytes)
signer := httpsig.NewEd25519Signer(keyId, key, nil)
if err := signer.Sign(r); err != nil {
return err
}
} else {
key, err := x509.ParsePKCS1PrivateKey(privateKeyBytes)
if err != nil {
return err
}
signer := httpsig.NewRSASHA256Signer(keyId, key, nil)
if err = signer.Sign(r); err != nil {
return err
}
// r.Header.Add("Signature", strings.TrimPrefix(r.Header.Get("Authorization"), "Signature "))
}
return nil
}
func applyBodyHash(headers http.Header, body []byte) error {
if body == nil {
return nil
}
hash := sha256.Sum256(body)
based := base64.StdEncoding.EncodeToString(hash[:])
headers.Set("Digest", "SHA-256="+based)
return nil
}
func NewRequest(method string, url string, body io.Reader) (*http.Request, error) { func NewRequest(method string, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, url, body) req, err := http.NewRequest(method, url, body)
if err != nil { if err != nil {
@ -130,7 +75,6 @@ func NewRequest(method string, url string, body io.Reader) (*http.Request, error
"User-Agent", "User-Agent",
"Linstrom v0.0.0-pre-alpha ("+config.GlobalConfig.General.GetFullDomain()+")", "Linstrom v0.0.0-pre-alpha ("+config.GlobalConfig.General.GetFullDomain()+")",
) )
req.Header.Add("Date", time.Now().Format(time.RFC1123)) req.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat))
req.Header.Add("Host", config.GlobalConfig.General.GetFullDomain())
return req, nil return req, nil
} }

View file

@ -1,13 +1,22 @@
package webshared package webshared
import ( import (
"bytes"
"crypto"
"crypto/ed25519"
"crypto/sha256"
"crypto/x509" "crypto/x509"
"io" "encoding/base64"
"net/http" "net/http"
"slices"
"time"
"github.com/go-fed/httpsig"
"github.com/rs/zerolog/log"
"github.com/yaronf/httpsign" "github.com/yaronf/httpsign"
"git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/config"
"git.mstar.dev/mstar/linstrom/shared"
) )
/* /*
@ -22,16 +31,19 @@ Links for home:
- https://datatracker.ietf.org/doc/html/rfc9421 - https://datatracker.ietf.org/doc/html/rfc9421
*/ */
func RequestSigned( // Perform a request, signing it as specified in RFC 9421
func RequestSignedRFC9421(
method, target string, method, target string,
body io.Reader, body []byte,
keyId string, keyId string,
privateKeyBytes []byte, privateKeyBytes []byte,
useEd bool,
) (*http.Response, error) { ) (*http.Response, error) {
req, err := http.NewRequest(method, target, body) req, err := http.NewRequest(method, target, bytes.NewBuffer(slices.Clone(body)))
if err != nil { if err != nil {
return nil, err return nil, err
} }
applyDefaultHeaders(req)
var signer *httpsign.Signer var signer *httpsign.Signer
signerFields := httpsign.Headers("@request-target", "content-digest") signerFields := httpsign.Headers("@request-target", "content-digest")
if config.GlobalConfig.Experimental.UseEd25519Keys { if config.GlobalConfig.Experimental.UseEd25519Keys {
@ -40,12 +52,18 @@ func RequestSigned(
httpsign.NewSignConfig(), httpsign.NewSignConfig(),
signerFields, signerFields,
) )
if err != nil {
return nil, err
}
} else { } else {
key, err := x509.ParsePKCS1PrivateKey(privateKeyBytes) key, err := x509.ParsePKCS1PrivateKey(privateKeyBytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signer, err = httpsign.NewRSASigner(*key, httpsign.NewSignConfig(), signerFields) signer, err = httpsign.NewRSASigner(*key, httpsign.NewSignConfig(), signerFields)
if err != nil {
return nil, err
}
} }
client := httpsign.NewClient( client := httpsign.NewClient(
RequestClient, RequestClient,
@ -54,3 +72,80 @@ func RequestSigned(
res, err := client.Do(req) res, err := client.Do(req)
return res, err return res, err
} }
// Perform a request, signing it as specified in the cavage proposal
// (https://swicg.github.io/activitypub-http-signature/#how-to-sign-a-request)
func RequestSignedCavage(
method, target string,
body []byte,
keyId string,
privateKeyBytes []byte,
useEd bool,
) (*http.Response, error) {
req, err := http.NewRequest(method, target, bytes.NewBuffer(slices.Clone(body)))
if err != nil {
return nil, err
}
applyDefaultHeaders(req)
var prefs []httpsig.Algorithm
var key crypto.PrivateKey
if useEd {
log.Debug().Msg("Using ed25519 cavage")
prefs = append(prefs, httpsig.ED25519)
key = ed25519.PrivateKey(privateKeyBytes)
} else {
log.Debug().Msg("Using RSA cavage")
// prefs = append(prefs, httpsig.RSA_SHA512, httpsig.RSA_SHA256)
prefs = append(prefs, httpsig.RSA_SHA256)
tempKey, err := x509.ParsePKCS1PrivateKey(privateKeyBytes)
if err != nil {
return nil, err
}
key = tempKey
}
digestAlgorithm := httpsig.DigestSha256
headersToSign := []string{httpsig.RequestTarget, "date", "host", "user-agent"}
if len(body) > 0 {
headersToSign = append(headersToSign, "digest")
log.Debug().Msg("Non-empty body, adding digest")
} else {
// Just to ensure the signer doesn't fuck up
body = nil
}
signer, chosenAlgorithm, err := httpsig.NewSigner(
prefs,
digestAlgorithm,
headersToSign,
httpsig.Signature,
int64(time.Hour),
)
if err != nil {
return nil, err
}
log.Debug().Any("algorithm", chosenAlgorithm).Msg("Signer chose algorithm")
if err = signer.SignRequest(key, keyId, req, body); err != nil {
return nil, err
}
log.Debug().Any("headers", req.Header).Msg("Request post signing")
return RequestClient.Do(req)
}
func applyDefaultHeaders(r *http.Request) {
r.Header.Add(
"User-Agent",
"Linstrom "+shared.Version+" ("+config.GlobalConfig.General.GetFullDomain()+")",
)
r.Header.Add("Date", time.Now().UTC().Format(http.TimeFormat))
r.Header.Add("Host", config.GlobalConfig.General.GetFullDomain())
r.Header.Add("Accept", "application/activity+json")
}
func applyBodyHash(headers http.Header, body []byte) error {
if len(body) == 0 {
return nil
}
hash := sha256.Sum256(body)
based := base64.StdEncoding.EncodeToString(hash[:])
headers.Set("Digest", "SHA-256="+based)
return nil
}

View file

@ -2,6 +2,7 @@ package webshared
import ( import (
"encoding/base64" "encoding/base64"
"net/url"
"strings" "strings"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -15,7 +16,7 @@ import (
// result // result
func CreateSignatureRSA( func CreateSignatureRSA(
method string, method string,
target string, target *url.URL,
headers map[string]string, headers map[string]string,
privateKeyBytes []byte, privateKeyBytes []byte,
) (string, []string, error) { ) (string, []string, error) {
@ -40,7 +41,7 @@ func CreateSignatureRSA(
// encoded result // encoded result
func CreateSignatureED( func CreateSignatureED(
method string, method string,
target string, target *url.URL,
headers map[string]string, headers map[string]string,
privateKeyBytes []byte, privateKeyBytes []byte,
) (string, []string, error) { ) (string, []string, error) {
@ -52,14 +53,20 @@ func CreateSignatureED(
return base64.StdEncoding.EncodeToString(signed), usedHeaders, nil return base64.StdEncoding.EncodeToString(signed), usedHeaders, nil
} }
func genPreSignatureString(method, target string, headers map[string]string) (string, []string) { func genPreSignatureString(
method string,
target *url.URL,
headers map[string]string,
) (string, []string) {
dataBuilder := strings.Builder{} dataBuilder := strings.Builder{}
dataBuilder.WriteString("(request-target): ") dataBuilder.WriteString("(request-target): ")
dataBuilder.WriteString(strings.ToLower(method) + " ") dataBuilder.WriteString(strings.ToLower(method) + " ")
dataBuilder.WriteString(target + "\n") dataBuilder.WriteString(target.Path + "\n")
dataBuilder.WriteString("host: ")
dataBuilder.WriteString(target.Host + "\n")
// dataBuilder.WriteString("algorithm: rsa-sha256\n") // dataBuilder.WriteString("algorithm: rsa-sha256\n")
// usedHeaders := []string{"(request-target)", "algorithm"} // usedHeaders := []string{"(request-target)", "algorithm"}
usedHeaders := []string{"(request-target)"} usedHeaders := []string{"(request-target)", "host"}
for k, v := range headers { for k, v := range headers {
dataBuilder.WriteString(k + ": " + v + "\n") dataBuilder.WriteString(k + ": " + v + "\n")
usedHeaders = append(usedHeaders, k) usedHeaders = append(usedHeaders, k)