models/user: add function to get FederatedUser by local userID

This commit is contained in:
famfo 2025-02-28 17:02:50 +01:00
parent 1af31f86da
commit b29449c9c6
No known key found for this signature in database
4 changed files with 181 additions and 17 deletions

View file

@ -4,9 +4,11 @@
package user package user
import ( import (
"context"
"fmt" "fmt"
"net/url" "net/url"
"forgejo.org/models/db"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
) )
@ -23,3 +25,15 @@ func (u *User) APActorID() string {
func (u *User) APActorKeyID() string { func (u *User) APActorKeyID() string {
return u.APActorID() + "#main-key" return u.APActorID() + "#main-key"
} }
func GetUserByFederatedURI(ctx context.Context, federatedURI string) (*User, error) {
user := new(User)
has, err := db.GetEngine(ctx).Where("normalized_federated_uri=?", federatedURI).Get(user)
if err != nil {
return nil, err
} else if !has {
return nil, nil
}
return user, nil
}

View file

@ -32,16 +32,24 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64) (
return result, nil return result, nil
} }
func GetFederatedUserByKeyID(ctx context.Context, keyID *url.URL) (*FederatedUser, error) { func GetFederatedUserFromDB(ctx context.Context, searchKey, searchValue any) (*FederatedUser, error) {
user := new(FederatedUser) federatedUser := new(FederatedUser)
has, err := db.GetEngine(ctx).Where("key_id=?", keyID).Get(user) has, err := db.GetEngine(ctx).Where(searchKey, searchValue).Get(federatedUser)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
return nil, nil return nil, nil
} }
return user, nil return federatedUser, nil
}
func GetFederatedUserByKeyID(ctx context.Context, keyID string) (*FederatedUser, error) {
return GetFederatedUserFromDB(ctx, "key_id=?", keyID)
}
func GetFederatedUserByUserID(ctx context.Context, userID int64) (*FederatedUser, error) {
return GetFederatedUserFromDB(ctx, "user_id=?", userID)
} }
func (user FederatedUser) Validate() []string { func (user FederatedUser) Validate() []string {

View file

@ -6,37 +6,130 @@ package activitypub
import ( import (
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"database/sql"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"forgejo.org/models/db"
"forgejo.org/models/forgefed"
"forgejo.org/models/user" "forgejo.org/models/user"
"forgejo.org/modules/activitypub" "forgejo.org/modules/activitypub"
fm "forgejo.org/modules/forgefed"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
gitea_context "forgejo.org/services/context" gitea_context "forgejo.org/services/context"
"forgejo.org/services/federation"
"github.com/42wim/httpsig" "github.com/42wim/httpsig"
ap "github.com/go-ap/activitypub" ap "github.com/go-ap/activitypub"
) )
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) { func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
block, _ := pem.Decode([]byte(pubKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
return nil, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
}
return block.Bytes, nil
}
func getFederatedUser(ctx *gitea_context.APIContext, person *ap.Person, federationHost *forgefed.FederationHost) (*user.FederatedUser, error) {
dbUser, err := user.GetUserByFederatedURI(ctx, person.ID.String())
if err != nil {
return nil, err
}
if dbUser != nil {
federatedUser, err := user.GetFederatedUserByUserID(ctx, dbUser.ID)
if err != nil {
return nil, err
}
if federatedUser != nil {
return federatedUser, nil
}
}
personID, err := fm.NewPersonID(person.ID.String(), string(federationHost.NodeInfo.SoftwareName))
if err != nil {
return nil, err
}
_, federatedUser, err := federation.CreateUserFromAP(ctx, personID, federationHost.ID)
if err != nil {
return nil, err
}
return federatedUser, nil
}
func getPublicKeyFromResponse(ctx *gitea_context.APIContext, b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {
person := ap.PersonNew(ap.IRI(keyID.String())) person := ap.PersonNew(ap.IRI(keyID.String()))
err = person.UnmarshalJSON(b) err = person.UnmarshalJSON(b)
if err != nil { if err != nil {
return nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err) return nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
} }
pubKey := person.PublicKey pubKey := person.PublicKey
if pubKey.ID.String() != keyID.String() { if pubKey.ID.String() != keyID.String() {
return nil, fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b)) return nil, fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b))
} }
pubKeyPem := pubKey.PublicKeyPem
block, _ := pem.Decode([]byte(pubKeyPem)) pubKeyBytes, err := decodePublicKeyPem(pubKey.PublicKeyPem)
if block == nil || block.Type != "PUBLIC KEY" { if err != nil {
return nil, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type") return nil, err
} }
p, err = x509.ParsePKIXPublicKey(block.Bytes)
p, err = x509.ParsePKIXPublicKey(pubKeyBytes)
if err != nil {
return nil, err
}
// Cache siging key in the database
federationHost, err := federation.GetFederationHostForURI(ctx, person.ID.String())
if err != nil {
return nil, err
}
if person.Type == ap.ActivityVocabularyType("Application") {
federationHost.KeyID = sql.NullString{
String: keyID.String(),
Valid: true,
}
federationHost.PublicKey = sql.Null[sql.RawBytes]{
V: pubKeyBytes,
Valid: true,
}
_, err = db.GetEngine(ctx).ID(federationHost.ID).Update(federationHost)
if err != nil {
return nil, err
}
} else if person.Type == ap.ActivityVocabularyType("Person") {
federatedUser, err := getFederatedUser(ctx, person, federationHost)
if err != nil {
return nil, err
}
federatedUser.KeyID = sql.NullString{
String: keyID.String(),
Valid: true,
}
federatedUser.PublicKey = sql.Null[sql.RawBytes]{
V: pubKeyBytes,
Valid: true,
}
_, err = db.GetEngine(ctx).ID(federatedUser.ID).Update(federatedUser)
if err != nil {
return nil, err
}
}
return p, err return p, err
} }
@ -59,9 +152,42 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er
return false, err return false, err
} }
log.Debug("Fetching keyID: %s", ID) signatureAlgorithm := httpsig.Algorithm(setting.Federation.SignatureAlgorithms[0])
// 2. Fetch the public key of the other actor // 2. Fetch the public key of the other actor
// Try if the signing actor is an already known federated user
federationUser, err := user.GetFederatedUserByKeyID(ctx, idIRI.String())
if err != nil {
return false, err
}
if federationUser != nil && federationUser.PublicKey.Valid {
pubKey, err := x509.ParsePKIXPublicKey(federationUser.PublicKey.V)
if err != nil {
return false, err
}
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
return authenticated, err
}
// Try if the signing actor is an already known federation host
federationHost, err := forgefed.FindFederationHostByKeyID(ctx, idIRI.String())
if err != nil {
return false, err
}
if federationHost != nil && federationHost.PublicKey.Valid {
pubKey, err := x509.ParsePKIXPublicKey(federationHost.PublicKey.V)
if err != nil {
return false, err
}
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
return authenticated, err
}
// Fetch missing public key
actionsUser := user.NewAPActorUser() actionsUser := user.NewAPActorUser()
clientFactory, err := activitypub.GetClientFactory(ctx) clientFactory, err := activitypub.GetClientFactory(ctx)
if err != nil { if err != nil {
@ -78,14 +204,12 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er
return false, err return false, err
} }
pubKey, err := getPublicKeyFromResponse(b, idIRI) pubKey, err := getPublicKeyFromResponse(ctx, b, idIRI)
if err != nil { if err != nil {
return false, err return false, err
} }
// 3. Verify the other actor's key authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
algo := httpsig.Algorithm(setting.Federation.SignatureAlgorithms[0])
authenticated = v.Verify(pubKey, algo) == nil
return authenticated, err return authenticated, err
} }

View file

@ -10,8 +10,9 @@ import (
"testing" "testing"
"forgejo.org/models/db" "forgejo.org/models/db"
"forgejo.org/models/forgefed"
"forgejo.org/models/unittest" "forgejo.org/models/unittest"
user_model "forgejo.org/models/user" "forgejo.org/models/user"
"forgejo.org/modules/activitypub" "forgejo.org/modules/activitypub"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/test" "forgejo.org/modules/test"
@ -29,7 +30,7 @@ func TestFederationHttpSigValidation(t *testing.T) {
userID := 2 userID := 2
userURL := fmt.Sprintf("%vapi/v1/activitypub/user-id/%v", u, userID) userURL := fmt.Sprintf("%vapi/v1/activitypub/user-id/%v", u, userID)
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) user1 := unittest.AssertExistsAndLoadBean(t, &user.User{ID: 1})
clientFactory, err := activitypub.GetClientFactory(db.DefaultContext) clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
require.NoError(t, err) require.NoError(t, err)
@ -46,6 +47,23 @@ func TestFederationHttpSigValidation(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
// HACK HACK HACK: the host part of the URL gets set to which IP forgejo is
// listening on, NOT localhost, which is the Domain given to forgejo which
// is then used for eg. the keyID all requests
applicationKeyID := fmt.Sprintf("http://localhost:%v/api/v1/activitypub/actor#main-key", u.Port())
actorKeyID := fmt.Sprintf("http://localhost:%v/api/v1/activitypub/user-id/1#main-key", u.Port())
// Check for cached public keys
host, err := forgefed.FindFederationHostByKeyID(db.DefaultContext, applicationKeyID)
require.NoError(t, err)
assert.NotNil(t, host)
assert.True(t, host.PublicKey.Valid)
user, err := user.GetFederatedUserByKeyID(db.DefaultContext, actorKeyID)
require.NoError(t, err)
assert.NotNil(t, user)
assert.True(t, user.PublicKey.Valid)
// Disable signature validation // Disable signature validation
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)() defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()