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 }