mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-07-07 09:55:41 +02:00
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
This PR is part of https://codeberg.org/forgejo/forgejo/pulls/4767 It improves the signature handling: 1. move logic to a service (might be used from other services as well) 2. make a clear difference between ` ReqHTTPUserSignature` and `ReqHTTPUserOrInstanceSignature` 3. improve test ability (activitypub/client & distant_federation_server_mock Adjust instance actor 1. name & 2. webfinger ## Strategy for next PRs is Integration tests are in the driving seat. I will step by step add integration tests form original PR and add code required by the integration test changes. ## Meta Proposal howto process large PRs can be discussed here: https://codeberg.org/forgejo-contrib/federation/pulls/37 Current state with rendered diagrams can be found here: https://codeberg.org/meissa/federation/src/branch/merge-large-pr/doc/merge-large-pr.md Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8275 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Michael Jerger <michael.jerger@meissa-gmbh.de> Co-committed-by: Michael Jerger <michael.jerger@meissa-gmbh.de>
234 lines
6 KiB
Go
234 lines
6 KiB
Go
// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package federation
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"forgejo.org/models/forgefed"
|
|
"forgejo.org/models/user"
|
|
"forgejo.org/modules/activitypub"
|
|
fm "forgejo.org/modules/forgefed"
|
|
context_service "forgejo.org/services/context"
|
|
|
|
ap "github.com/go-ap/activitypub"
|
|
)
|
|
|
|
// Factory function for ActorID. Created struct is asserted to be valid
|
|
func NewActorIDFromKeyID(ctx *context_service.Base, uri string) (fm.ActorID, error) {
|
|
parsedURI, err := url.Parse(uri)
|
|
parsedURI.Fragment = ""
|
|
if err != nil {
|
|
return fm.ActorID{}, err
|
|
}
|
|
|
|
actionsUser := user.NewAPServerActor()
|
|
clientFactory, err := activitypub.GetClientFactory(ctx)
|
|
if err != nil {
|
|
return fm.ActorID{}, err
|
|
}
|
|
|
|
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
|
|
if err != nil {
|
|
return fm.ActorID{}, err
|
|
}
|
|
|
|
userResponse, err := apClient.GetBody(parsedURI.String())
|
|
if err != nil {
|
|
return fm.ActorID{}, err
|
|
}
|
|
|
|
var actor ap.Actor
|
|
err = actor.UnmarshalJSON(userResponse)
|
|
if err != nil {
|
|
return fm.ActorID{}, err
|
|
}
|
|
|
|
result, err := fm.NewActorID(actor.PublicKey.Owner.String())
|
|
return result, err
|
|
}
|
|
|
|
func FindOrCreateFederatedUserKey(ctx *context_service.Base, keyID string) (pubKey any, err error) {
|
|
var federatedUser *user.FederatedUser
|
|
var keyURL *url.URL
|
|
|
|
keyURL, err = url.Parse(keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Try if the signing actor is an already known federated user
|
|
_, federatedUser, err = user.FindFederatedUserByKeyID(ctx, keyURL.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if federatedUser == nil {
|
|
rawActorID, err := NewActorIDFromKeyID(ctx, keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, federatedUser, _, err = FindOrCreateFederatedUser(ctx, rawActorID.AsURI())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
_, err = forgefed.GetFederationHost(ctx, federatedUser.FederationHostID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if federatedUser.PublicKey.Valid {
|
|
pubKey, err := x509.ParsePKIXPublicKey(federatedUser.PublicKey.V)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pubKey, nil
|
|
}
|
|
|
|
// Fetch missing public key
|
|
pubKey, pubKeyBytes, apPerson, err := fetchKeyFromAp(ctx, *keyURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if apPerson.Type == ap.ActivityVocabularyType("Person") {
|
|
// Check federatedUser.id = person.id
|
|
if federatedUser.ExternalID != apPerson.ID.String() {
|
|
return nil, fmt.Errorf("federated user fetched (%v) does not match the stored one %v", apPerson, federatedUser)
|
|
}
|
|
// update federated user
|
|
federatedUser.KeyID = sql.NullString{
|
|
String: apPerson.PublicKey.ID.String(),
|
|
Valid: true,
|
|
}
|
|
federatedUser.PublicKey = sql.Null[sql.RawBytes]{
|
|
V: pubKeyBytes,
|
|
Valid: true,
|
|
}
|
|
err = user.UpdateFederatedUser(ctx, federatedUser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pubKey, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func FindOrCreateFederationHostKey(ctx *context_service.Base, keyID string) (pubKey any, err error) {
|
|
keyURL, err := url.Parse(keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rawActorID, err := NewActorIDFromKeyID(ctx, keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Is there an already known federation host?
|
|
federationHost, err := forgefed.FindFederationHostByKeyID(ctx, keyURL.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if federationHost == nil {
|
|
federationHost, err = FindOrCreateFederationHost(ctx, rawActorID.AsURI())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Is there an already an key?
|
|
if federationHost.PublicKey.Valid {
|
|
pubKey, err := x509.ParsePKIXPublicKey(federationHost.PublicKey.V)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pubKey, nil
|
|
}
|
|
|
|
// If not, fetch missing public key
|
|
pubKey, pubKeyBytes, apPerson, err := fetchKeyFromAp(ctx, *keyURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if apPerson.Type == ap.ActivityVocabularyType("Application") {
|
|
// Check federationhost.id = person.id
|
|
if federationHost.HostPort != rawActorID.HostPort || federationHost.HostFqdn != rawActorID.Host ||
|
|
federationHost.HostSchema != rawActorID.HostSchema {
|
|
return nil, fmt.Errorf("federation host fetched (%v) does not match the stored one %v", apPerson, federationHost)
|
|
}
|
|
// update federation host
|
|
federationHost.KeyID = sql.NullString{
|
|
String: apPerson.PublicKey.ID.String(),
|
|
Valid: true,
|
|
}
|
|
federationHost.PublicKey = sql.Null[sql.RawBytes]{
|
|
V: pubKeyBytes,
|
|
Valid: true,
|
|
}
|
|
err = forgefed.UpdateFederationHost(ctx, federationHost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pubKey, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func fetchKeyFromAp(ctx *context_service.Base, keyURL url.URL) (pubKey any, pubKeyBytes []byte, apPerson *ap.Person, err error) {
|
|
actionsUser := user.NewAPServerActor()
|
|
clientFactory, err := activitypub.GetClientFactory(ctx)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
b, err := apClient.GetBody(keyURL.String())
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
person := ap.PersonNew(ap.IRI(keyURL.String()))
|
|
err = person.UnmarshalJSON(b)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
|
|
}
|
|
|
|
pubKeyFromAp := person.PublicKey
|
|
if pubKeyFromAp.ID.String() != keyURL.String() {
|
|
return nil, nil, nil, fmt.Errorf("cannot find publicKey with id: %v in %v", keyURL, string(b))
|
|
}
|
|
|
|
pubKeyBytes, err = decodePublicKeyPem(pubKeyFromAp.PublicKeyPem)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
pubKey, err = x509.ParsePKIXPublicKey(pubKeyBytes)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
return pubKey, pubKeyBytes, person, err
|
|
}
|
|
|
|
func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
|
|
block, _ := pem.Decode([]byte(pubKeyPem))
|
|
if block == nil || block.Type != "PUBLIC KEY" {
|
|
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
|
}
|
|
|
|
return block.Bytes, nil
|
|
}
|