Files
2026-05-12 20:55:13 +02:00

176 lines
5.0 KiB
Go

package federation
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
"strings"
"time"
"xorm.io/xorm"
)
// Sign adds an HTTP Signature header to req using the RSA private key.
// Implements draft-cavage-http-signatures (the fediverse de-facto standard).
// Signs: (request-target), host, date. If body is set, also signs digest.
func Sign(req *http.Request, keyID, privateKeyPEM string) error {
if req.Header.Get("Date") == "" {
req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
}
if req.Header.Get("Host") == "" {
req.Header.Set("Host", req.URL.Host)
}
method := strings.ToLower(req.Method)
target := req.URL.RequestURI()
signingString := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s",
method, target,
req.Header.Get("Host"),
req.Header.Get("Date"),
)
headers := "(request-target) host date"
priv, err := parsePrivateKey(privateKeyPEM)
if err != nil {
return fmt.Errorf("parse private key: %w", err)
}
h := sha256.Sum256([]byte(signingString))
sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, h[:])
if err != nil {
return fmt.Errorf("sign: %w", err)
}
req.Header.Set("Signature", fmt.Sprintf(
`keyId="%s",algorithm="rsa-sha256",headers="%s",signature="%s"`,
keyID, headers, base64.StdEncoding.EncodeToString(sig),
))
return nil
}
// Verify validates the HTTP Signature header on an incoming request.
// It fetches the sender's public key from their actor document (or the local DB).
func Verify(r *http.Request, db *xorm.Engine, instanceURL string) error {
sigHeader := r.Header.Get("Signature")
if sigHeader == "" {
return fmt.Errorf("missing Signature header")
}
params := parseSignatureHeader(sigHeader)
keyID := params["keyId"]
sigB64 := params["signature"]
headersList := params["headers"]
if keyID == "" || sigB64 == "" {
return fmt.Errorf("malformed Signature header")
}
sig, err := base64.StdEncoding.DecodeString(sigB64)
if err != nil {
return fmt.Errorf("decode signature: %w", err)
}
// Fetch the public key for this keyId.
// keyId is typically "{actorURL}#main-key" — strip the fragment to get the actor APID.
actorAPID := strings.SplitN(keyID, "#", 2)[0]
pubKeyPEM, err := resolvePublicKey(db, actorAPID, instanceURL)
if err != nil {
return fmt.Errorf("resolve public key for %s: %w", actorAPID, err)
}
// Reconstruct the signing string from the request.
signedHeaders := strings.Fields(headersList)
if len(signedHeaders) == 0 {
signedHeaders = []string{"date"}
}
var parts []string
for _, h := range signedHeaders {
switch h {
case "(request-target)":
parts = append(parts, fmt.Sprintf("(request-target): %s %s",
strings.ToLower(r.Method), r.URL.RequestURI()))
default:
parts = append(parts, h+": "+r.Header.Get(http.CanonicalHeaderKey(h)))
}
}
signingString := strings.Join(parts, "\n")
pub, err := parsePublicKey(pubKeyPEM)
if err != nil {
return fmt.Errorf("parse public key: %w", err)
}
h := sha256.Sum256([]byte(signingString))
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, h[:], sig); err != nil {
return fmt.Errorf("signature verification failed: %w", err)
}
return nil
}
// ── helpers ──────────────────────────────────────────────────────────────────
func parseSignatureHeader(header string) map[string]string {
params := make(map[string]string)
for _, part := range strings.Split(header, ",") {
part = strings.TrimSpace(part)
idx := strings.Index(part, "=")
if idx < 0 {
continue
}
key := strings.TrimSpace(part[:idx])
val := strings.Trim(strings.TrimSpace(part[idx+1:]), `"`)
params[key] = val
}
return params
}
func parsePrivateKey(pemStr string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(pemStr))
if block == nil {
return nil, fmt.Errorf("no PEM block found")
}
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
func parsePublicKey(pemStr string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pemStr))
if block == nil {
return nil, fmt.Errorf("no PEM block found")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("not an RSA public key")
}
return rsaPub, nil
}
// resolvePublicKey returns the public key PEM for an actor APID.
// Checks local actors first, then remote cache, then fetches from network.
func resolvePublicKey(db *xorm.Engine, actorAPID, instanceURL string) (string, error) {
// Check if it's a local actor.
var local struct {
PublicKey string `xorm:"public_key"`
}
if found, _ := db.Table("federation_actor").
Where("ap_id = ?", actorAPID).
Cols("public_key").Get(&local); found && local.PublicKey != "" {
return local.PublicKey, nil
}
// Fetch (and cache) from network.
remote, err := FetchActor(db, actorAPID)
if err != nil {
return "", err
}
return remote.PublicKey, nil
}