implemented federation
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user