mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Allow admins to expire remote public keys; refetch expired keys on demand (#2183)
This commit is contained in:
@@ -25,14 +25,17 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-kv"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
@@ -45,11 +48,47 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// PubKeyAuth models authorization information for a remote
|
||||
// Actor making a signed HTTP request to this GtS instance
|
||||
// using a public key.
|
||||
type PubKeyAuth struct {
|
||||
// CachedPubKey is the public key found in the db
|
||||
// for the Actor whose request we're now authenticating.
|
||||
// Will be set only in cases where we had the Owner
|
||||
// of the key stored in the database already.
|
||||
CachedPubKey *rsa.PublicKey
|
||||
|
||||
// FetchedPubKey is an up-to-date public key fetched
|
||||
// from the remote instance. Will be set in cases
|
||||
// where EITHER we hadn't seen the Actor before whose
|
||||
// request we're now authenticating, OR a CachedPubKey
|
||||
// was found in our database, but was expired.
|
||||
FetchedPubKey *rsa.PublicKey
|
||||
|
||||
// OwnerURI is the ActivityPub id of the owner of
|
||||
// the public key used to sign the request we're
|
||||
// now authenticating. This will always be set
|
||||
// even if Owner isn't, so that callers can use
|
||||
// this URI to go fetch the Owner from remote.
|
||||
OwnerURI *url.URL
|
||||
|
||||
// Owner is the account corresponding to OwnerURI.
|
||||
//
|
||||
// Owner will only be defined if the account who
|
||||
// owns the public key was already cached in the
|
||||
// database when we received the request we're now
|
||||
// authenticating (ie., we've seen it before).
|
||||
//
|
||||
// If it's not defined, callers should use OwnerURI
|
||||
// to go and dereference it.
|
||||
Owner *gtsmodel.Account
|
||||
}
|
||||
|
||||
// AuthenticateFederatedRequest authenticates any kind of incoming federated
|
||||
// request from a remote server. This includes things like GET requests for
|
||||
// dereferencing our users or statuses etc, and POST requests for delivering
|
||||
// new Activities. The function returns the URL of the owner of the public key
|
||||
// used in the requesting http signature.
|
||||
// new Activities. The function returns details of the public key(s) used to
|
||||
// authenticate the requesting http signature.
|
||||
//
|
||||
// 'Authenticate' in this case is defined as making sure that the http request
|
||||
// is actually signed by whoever claims to have signed it, by fetching the public
|
||||
@@ -70,7 +109,7 @@ var (
|
||||
// Also note that this function *does not* dereference the remote account that
|
||||
// the signature key is associated with. Other functions should use the returned
|
||||
// URL to dereference the remote account, if required.
|
||||
func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*url.URL, gtserror.WithCode) {
|
||||
func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*PubKeyAuth, gtserror.WithCode) {
|
||||
// Thanks to the signature check middleware,
|
||||
// we should already have an http signature
|
||||
// verifier set on the context. If we don't,
|
||||
@@ -102,10 +141,10 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
|
||||
// so now we need to validate the signature.
|
||||
|
||||
var (
|
||||
pubKeyIDStr = pubKeyID.String()
|
||||
requestingAccountURI *url.URL
|
||||
pubKey interface{}
|
||||
errWithCode gtserror.WithCode
|
||||
pubKeyIDStr = pubKeyID.String()
|
||||
local = (pubKeyID.Host == config.GetHost())
|
||||
pubKeyAuth *PubKeyAuth
|
||||
errWithCode gtserror.WithCode
|
||||
)
|
||||
|
||||
l := log.
|
||||
@@ -115,37 +154,49 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
|
||||
{"pubKeyID", pubKeyIDStr},
|
||||
}...)
|
||||
|
||||
if pubKeyID.Host == config.GetHost() {
|
||||
l.Trace("public key is ours, no dereference needed")
|
||||
requestingAccountURI, pubKey, errWithCode = f.derefDBOnly(ctx, pubKeyIDStr)
|
||||
if local {
|
||||
l.Trace("public key is local, no dereference needed")
|
||||
pubKeyAuth, errWithCode = f.derefPubKeyDBOnly(ctx, pubKeyIDStr)
|
||||
} else {
|
||||
l.Trace("public key is not ours, checking if we need to dereference")
|
||||
requestingAccountURI, pubKey, errWithCode = f.deref(ctx, requestedUsername, pubKeyIDStr, pubKeyID)
|
||||
l.Trace("public key is remote, checking if we need to dereference")
|
||||
pubKeyAuth, errWithCode = f.derefPubKey(ctx, requestedUsername, pubKeyIDStr, pubKeyID)
|
||||
}
|
||||
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
// Ensure public key now defined.
|
||||
if pubKey == nil {
|
||||
err := gtserror.New("public key was nil")
|
||||
if local && pubKeyAuth == nil {
|
||||
// We signed this request, apparently, but
|
||||
// local lookup didn't find anything. This
|
||||
// is an almost impossible error condition!
|
||||
err := gtserror.Newf("local public key %s could not be found; "+
|
||||
"has the account been manually removed from the db?", pubKeyIDStr)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Try to authenticate using permitted algorithms in
|
||||
// order of most -> least common. Return OK as soon
|
||||
// as one passes.
|
||||
for _, algo := range signingAlgorithms {
|
||||
l.Tracef("trying %s", algo)
|
||||
|
||||
err := verifier.Verify(pubKey, algo)
|
||||
if err == nil {
|
||||
l.Tracef("authentication PASSED with %s", algo)
|
||||
return requestingAccountURI, nil
|
||||
// order of most -> least common, checking each defined
|
||||
// pubKey for this Actor. Return OK as soon as one passes.
|
||||
for _, pubKey := range [2]*rsa.PublicKey{
|
||||
pubKeyAuth.FetchedPubKey,
|
||||
pubKeyAuth.CachedPubKey,
|
||||
} {
|
||||
if pubKey == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
l.Tracef("authentication NOT PASSED with %s: %q", algo, err)
|
||||
for _, algo := range signingAlgorithms {
|
||||
l.Tracef("trying %s", algo)
|
||||
|
||||
err := verifier.Verify(pubKey, algo)
|
||||
if err == nil {
|
||||
l.Tracef("authentication PASSED with %s", algo)
|
||||
return pubKeyAuth, nil
|
||||
}
|
||||
|
||||
l.Tracef("authentication NOT PASSED with %s: %q", algo, err)
|
||||
}
|
||||
}
|
||||
|
||||
// At this point no algorithms passed.
|
||||
@@ -157,36 +208,52 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
|
||||
return nil, gtserror.NewErrorUnauthorized(err, err.Error())
|
||||
}
|
||||
|
||||
// derefDBOnly tries to dereference the given public
|
||||
// key using only entries already in the database.
|
||||
func (f *federator) derefDBOnly(
|
||||
// derefPubKeyDBOnly tries to dereference the given
|
||||
// pubKey using only entries already in the database.
|
||||
//
|
||||
// In case of a db or URL error, will return the error.
|
||||
//
|
||||
// In case an entry for the pubKey owner just doesn't
|
||||
// exist in the db (yet), will return nil, nil.
|
||||
func (f *federator) derefPubKeyDBOnly(
|
||||
ctx context.Context,
|
||||
pubKeyIDStr string,
|
||||
) (*url.URL, interface{}, gtserror.WithCode) {
|
||||
reqAcct, err := f.db.GetAccountByPubkeyID(ctx, pubKeyIDStr)
|
||||
) (*PubKeyAuth, gtserror.WithCode) {
|
||||
owner, err := f.db.GetAccountByPubkeyID(ctx, pubKeyIDStr)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
// We don't have this
|
||||
// account stored (yet).
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err = gtserror.Newf("db error getting account with pubKeyID %s: %w", pubKeyIDStr, err)
|
||||
return nil, nil, gtserror.NewErrorInternalError(err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
reqAcctURI, err := url.Parse(reqAcct.URI)
|
||||
ownerURI, err := url.Parse(owner.URI)
|
||||
if err != nil {
|
||||
err = gtserror.Newf("error parsing account uri with pubKeyID %s: %w", pubKeyIDStr, err)
|
||||
return nil, nil, gtserror.NewErrorInternalError(err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return reqAcctURI, reqAcct.PublicKey, nil
|
||||
return &PubKeyAuth{
|
||||
CachedPubKey: owner.PublicKey,
|
||||
OwnerURI: ownerURI,
|
||||
Owner: owner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deref tries to dereference the given public key by first
|
||||
// checking in the database, and then (if no entries found)
|
||||
// calling the remote pub key URI and extracting the key.
|
||||
func (f *federator) deref(
|
||||
// derefPubKey tries to dereference the given public key by first
|
||||
// checking in the database, and then (if no entry found, or entry
|
||||
// found but pubKey expired) calling the remote pub key URI and
|
||||
// extracting the key.
|
||||
func (f *federator) derefPubKey(
|
||||
ctx context.Context,
|
||||
requestedUsername string,
|
||||
pubKeyIDStr string,
|
||||
pubKeyID *url.URL,
|
||||
) (*url.URL, interface{}, gtserror.WithCode) {
|
||||
) (*PubKeyAuth, gtserror.WithCode) {
|
||||
l := log.
|
||||
WithContext(ctx).
|
||||
WithFields(kv.Fields{
|
||||
@@ -196,42 +263,101 @@ func (f *federator) deref(
|
||||
|
||||
// Try a database only deref first. We may already
|
||||
// have the requesting account cached locally.
|
||||
reqAcctURI, pubKey, errWithCode := f.derefDBOnly(ctx, pubKeyIDStr)
|
||||
if errWithCode == nil {
|
||||
l.Trace("public key cached, no dereference needed")
|
||||
return reqAcctURI, pubKey, nil
|
||||
pubKeyAuth, errWithCode := f.derefPubKeyDBOnly(ctx, pubKeyIDStr)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
l.Trace("public key not cached, trying dereference")
|
||||
var (
|
||||
// Just haven't seen this
|
||||
// Actor + their pubkey yet.
|
||||
uncached = (pubKeyAuth == nil)
|
||||
|
||||
// Have seen this Actor + their
|
||||
// pubkey but latter is now expired.
|
||||
expired = (!uncached && pubKeyAuth.Owner.PubKeyExpired())
|
||||
)
|
||||
|
||||
switch {
|
||||
case uncached:
|
||||
l.Trace("public key was not cached, trying dereference of public key")
|
||||
case !expired:
|
||||
l.Trace("public key cached and up to date, no dereference needed")
|
||||
return pubKeyAuth, nil
|
||||
case expired:
|
||||
// This is fairly rare and it may be helpful for
|
||||
// admins to see what's going on, so log at info.
|
||||
l.Infof(
|
||||
"public key was cached, but expired at %s, trying dereference of new public key",
|
||||
pubKeyAuth.Owner.PublicKeyExpiresAt,
|
||||
)
|
||||
}
|
||||
|
||||
// If we've tried to get this account before and we
|
||||
// now have a tombstone for it (ie., it's been deleted
|
||||
// from remote), don't try to dereference it again.
|
||||
gone, err := f.CheckGone(ctx, pubKeyID)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error checking for tombstone for %s: %w", pubKeyIDStr, err)
|
||||
return nil, nil, gtserror.NewErrorInternalError(err)
|
||||
err := gtserror.Newf("error checking for tombstone (%s): %w", pubKeyIDStr, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if gone {
|
||||
err := gtserror.Newf("account with public key %s is gone", pubKeyIDStr)
|
||||
return nil, nil, gtserror.NewErrorGone(err)
|
||||
err := gtserror.Newf("account with public key is gone (%s)", pubKeyIDStr)
|
||||
return nil, gtserror.NewErrorGone(err)
|
||||
}
|
||||
|
||||
// Make an http call to get the pubkey.
|
||||
// Make an http call to get the (refreshed) pubkey.
|
||||
pubKeyBytes, errWithCode := f.callForPubKey(ctx, requestedUsername, pubKeyID)
|
||||
if errWithCode != nil {
|
||||
return nil, nil, errWithCode
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
// Extract the key and the owner from the response.
|
||||
pubKey, pubKeyOwner, err := parsePubKeyBytes(ctx, pubKeyBytes, pubKeyID)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error parsing public key %s: %w", pubKeyID, err)
|
||||
return nil, nil, gtserror.NewErrorUnauthorized(err)
|
||||
err := fmt.Errorf("error parsing public key (%s): %w", pubKeyID, err)
|
||||
return nil, gtserror.NewErrorUnauthorized(err)
|
||||
}
|
||||
|
||||
return pubKeyOwner, pubKey, nil
|
||||
if !expired {
|
||||
// PubKeyResponse was nil before because
|
||||
// we had nothing cached; return the key
|
||||
// we just fetched, and nothing else.
|
||||
return &PubKeyAuth{
|
||||
FetchedPubKey: pubKey,
|
||||
OwnerURI: pubKeyOwner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Add newly-fetched key to response.
|
||||
pubKeyAuth.FetchedPubKey = pubKey
|
||||
|
||||
// If key was expired, that means we already
|
||||
// had an owner stored for it locally. Since
|
||||
// we now successfully refreshed the pub key,
|
||||
// we should update the account to reflect that.
|
||||
ownerAcct := pubKeyAuth.Owner
|
||||
ownerAcct.PublicKey = pubKeyAuth.FetchedPubKey
|
||||
ownerAcct.PublicKeyExpiresAt = time.Time{}
|
||||
|
||||
l.Info("obtained a new public key to replace expired key, caching now; " +
|
||||
"authorization for this request will be attempted with both old and new keys")
|
||||
|
||||
if err := f.db.UpdateAccount(
|
||||
ctx,
|
||||
ownerAcct,
|
||||
"public_key",
|
||||
"public_key_expires_at",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating account with refreshed public key (%s): %w", pubKeyIDStr, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Return both new and cached (now
|
||||
// expired) keys, authentication
|
||||
// will be attempted with both.
|
||||
return pubKeyAuth, nil
|
||||
}
|
||||
|
||||
// callForPubKey handles the nitty gritty of actually
|
||||
|
Reference in New Issue
Block a user