diff options
Diffstat (limited to 'internal/federation/authenticate.go')
| -rw-r--r-- | internal/federation/authenticate.go | 159 |
1 files changed, 111 insertions, 48 deletions
diff --git a/internal/federation/authenticate.go b/internal/federation/authenticate.go index f8515e649..fabef822c 100644 --- a/internal/federation/authenticate.go +++ b/internal/federation/authenticate.go @@ -36,6 +36,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/id" "code.superseriousbusiness.org/gotosocial/internal/log" "code.superseriousbusiness.org/httpsig" "codeberg.org/gruf/go-kv/v2" @@ -103,7 +104,10 @@ type PubKeyAuth struct { // // Note that it is also valid to pass in an empty string here, in which case the // keys of the instance account will be used. -func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*PubKeyAuth, gtserror.WithCode) { +// +// The caller of this function MUST CHECK AT SOME POINT WHETHER THE PUB KEY OWNER +// HAS BEEN SUSPENDED, and handle it appropriately, as this function will not do so! +func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedUser 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, @@ -144,7 +148,7 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU l := log. WithContext(ctx). WithFields(kv.Fields{ - {"requestedUsername", requestedUsername}, + {"requestedUser", requestedUser}, {"pubKeyID", pubKeyIDStr}, }...) @@ -153,7 +157,7 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU pubKeyAuth, errWithCode = f.derefPubKeyDBOnly(ctx, pubKeyIDStr) } else { l.Trace("public key is remote, checking if we need to dereference") - pubKeyAuth, errWithCode = f.derefPubKey(ctx, requestedUsername, pubKeyIDStr, pubKeyID) + pubKeyAuth, errWithCode = f.derefPubKey(ctx, requestedUser, pubKeyIDStr, pubKeyID) } if errWithCode != nil { @@ -182,7 +186,7 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU // Ensure we have instance stored in // database for the account at URI. err := f.fetchAccountInstance(ctx, - requestedUsername, + requestedUser, pubKeyAuth.OwnerURI, ) if err != nil { @@ -192,7 +196,7 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU // If we're currently handshaking with another instance, return // without derefing the owner, the only possible time we do this. // This prevents deadlocks when GTS instances mutually deref. - if f.Handshaking(requestedUsername, pubKeyAuth.OwnerURI) { + if f.Handshaking(requestedUser, pubKeyAuth.OwnerURI) { log.Warnf(ctx, "network race during %s handshake", pubKeyAuth.OwnerURI) pubKeyAuth.Handshaking = true return pubKeyAuth, nil @@ -201,22 +205,14 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU // Dereference the account located at owner URI. // Use exact URI match, not URL match. pubKeyAuth.Owner, _, err = f.GetAccountByURI(ctx, - requestedUsername, + requestedUser, pubKeyAuth.OwnerURI, false, ) if err != nil { - if gtserror.StatusCode(err) == http.StatusGone { - // This can happen here instead of the pubkey 'gone' - // checks due to: the server sending account deletion - // notifications out, we start processing, the request above - // succeeds, and *then* the profile is removed and starts - // returning 410 Gone, at which point _this_ request fails. - return nil, gtserror.NewErrorGone(err) - } - - err := gtserror.Newf("error dereferencing account %s: %w", pubKeyAuth.OwnerURI, err) - return nil, gtserror.NewErrorInternalError(err) + // If we couldn't fetch the pub key owner either from + // the DB or remote, return appropriate error code. + return nil, keyOwnerFetchError(err, pubKeyAuth.OwnerURI) } // Catch a possible (but very rare) race condition where @@ -231,12 +227,68 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU } } - if !pubKeyAuth.Owner.SuspendedAt.IsZero() { - const text = "requesting account suspended" - return nil, gtserror.NewErrorForbidden(errors.New(text)) + return pubKeyAuth, nil +} + +// keyOwnerFetchError checks the given error for the +// attempt to fetch the given account URI, and wraps +// the error appropriately based on what went wrong. +func keyOwnerFetchError( + err error, + acctURI *url.URL, +) gtserror.WithCode { + var newErr error + + // Check if a status code was returned + // from the failed dereference attempt. + switch statusCode := gtserror.StatusCode(err); statusCode { + + case http.StatusUnauthorized: + // If we got 401 Unauthorized from the remote, + // something likely went wrong with signature + // verification. In this case we should also + // return unauthorized, as we can't validate. + // + // Unlike with forbidden, we should warn log + // about this, as it may indicate some kind + // of key mismatch necessitating admin action. + newErr = gtserror.Newf( + "received 401 Unauthorized fetching pub key owner %s: %w", + acctURI, err, + ) + + case http.StatusForbidden: + // If we got 403 Forbidden from the remote, + // we're not allowed to see the account making + // the request. In this case we should just + // return unauthorized, as we can't validate. + newErr = gtserror.Newf( + "received 403 Forbidden fetching pub key owner %s: %w", + acctURI, err, + ) + + case http.StatusGone: + // This can happen here instead of the pubkey + // 'gone' checks due to: the server sending account + // deletion notifications out, we start processing, + // the request above succeeds, and *then* the profile + // is removed and starts returning 410 Gone, at + // which point _this_ request fails. + newErr = gtserror.Newf( + "received 410 Gone fetching pub key owner %s: %w", + acctURI, err, + ) + + default: + // Handle all other errors + // (server not available etc). + newErr = gtserror.Newf( + "could not dereference pub key owner %s: %w", + acctURI, err, + ) } - return pubKeyAuth, nil + return gtserror.NewErrorUnauthorized(newErr) } // derefPubKeyDBOnly tries to dereference the given @@ -286,7 +338,7 @@ func (f *Federator) derefPubKeyDBOnly( // extracting the key. func (f *Federator) derefPubKey( ctx context.Context, - requestedUsername string, + requestedUser string, pubKeyIDStr string, pubKeyID *url.URL, ) ( @@ -296,10 +348,27 @@ func (f *Federator) derefPubKey( l := log. WithContext(ctx). WithFields(kv.Fields{ - {"requestedUsername", requestedUsername}, + {"requestedUser", requestedUser}, {"pubKeyID", pubKeyIDStr}, }...) + // If we've tried to get this pub key 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.db.TombstoneExistsWithURI(ctx, pubKeyIDStr) + if err != nil { + err := gtserror.Newf("error checking for tombstone (%s): %w", pubKeyIDStr, err) + return nil, gtserror.NewErrorInternalError(err) + } + + if gone { + // If we had a tombstone for the remote, set an HTTP + // response code of 410 Gone on the returned errWithCode. + err := gtserror.Newf("account with public key %s is gone: %w", pubKeyID, err) + err = gtserror.WithStatusCode(err, http.StatusGone) + return nil, gtserror.NewErrorUnauthorized(err) + } + // Try a database only deref first. We may already // have the requesting account cached locally. pubKeyAuth, errWithCode := f.derefPubKeyDBOnly(ctx, pubKeyIDStr) @@ -332,22 +401,8 @@ func (f *Federator) derefPubKey( ) } - // 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 (%s): %w", pubKeyIDStr, err) - return nil, gtserror.NewErrorInternalError(err) - } - - if gone { - err := gtserror.Newf("account with public key is gone (%s)", pubKeyIDStr) - return nil, gtserror.NewErrorGone(err) - } - // Make an http call to get the (refreshed) pubkey. - pubKeyBytes, errWithCode := f.callForPubKey(ctx, requestedUsername, pubKeyID) + pubKeyBytes, errWithCode := f.callForPubKey(ctx, requestedUser, pubKeyID) if errWithCode != nil { return nil, errWithCode } @@ -399,10 +454,10 @@ func (f *Federator) derefPubKey( // callForPubKey handles the nitty gritty of actually // making a request for the given pubKeyID with a -// transport created on behalf of requestedUsername. +// transport created on behalf of requestedUser. func (f *Federator) callForPubKey( ctx context.Context, - requestedUsername string, + requestedUser string, pubKeyID *url.URL, ) ([]byte, gtserror.WithCode) { // Use a transport to dereference the remote. @@ -410,10 +465,10 @@ func (f *Federator) callForPubKey( // We're on a hot path: don't retry if req fails. gtscontext.SetFastFail(ctx), - requestedUsername, + requestedUser, ) if err != nil { - err = gtserror.Newf("error creating transport for %s: %w", requestedUsername, err) + err = gtserror.Newf("error creating transport for %s: %w", requestedUser, err) return nil, gtserror.NewErrorInternalError(err) } @@ -439,13 +494,21 @@ func (f *Federator) callForPubKey( // (account deleted, moved, etc). Add a tombstone // to our database so that we can avoid trying to // dereference it in future. - if err := f.HandleGone(ctx, pubKeyID); err != nil { - err := gtserror.Newf("error marking public key %s as gone: %w", pubKeyID, err) + pubKeyIDStr := pubKeyID.String() + tombstone := >smodel.Tombstone{ + ID: id.NewULID(), + Domain: pubKeyID.Host, + URI: pubKeyIDStr, + } + if err := f.db.PutTombstone(ctx, tombstone); err != nil && !errors.Is(err, db.ErrAlreadyExists) { + err := gtserror.Newf("db error adding tombstone for pub key %s: %w", pubKeyIDStr, err) return nil, gtserror.NewErrorInternalError(err) } - err := gtserror.Newf("account with public key %s is gone", pubKeyID) - return nil, gtserror.NewErrorGone(err) + // Wrap the error to preserve the 410 Gone status code. + err := gtserror.Newf("account with public key %s is gone: %w", pubKeyID, err) + err = gtserror.WithStatusCode(err, http.StatusGone) + return nil, gtserror.NewErrorUnauthorized(err) } // Fall back to generic error. @@ -457,7 +520,7 @@ func (f *Federator) callForPubKey( // the database for the given account URI, deref'ing if necessary. func (f *Federator) fetchAccountInstance( ctx context.Context, - requestedUsername string, + requestedUser string, accountURI *url.URL, ) error { // Look for an existing entry for instance in database. @@ -475,7 +538,7 @@ func (f *Federator) fetchAccountInstance( // instance yet; go dereference it. instance, err = f.GetRemoteInstance( gtscontext.SetFastFail(ctx), - requestedUsername, + requestedUser, &url.URL{ Scheme: accountURI.Scheme, Host: accountURI.Host, |
