[chore] Return more useful errors from auth failure (#494)

* try rsa_sha256 sig algo first

* return more informative errors from auth

* adapt to reworked auth function
This commit is contained in:
tobi 2022-04-26 18:10:11 +02:00 committed by GitHub
parent 728c4a5e38
commit 9cf66bf298
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 92 additions and 65 deletions

View file

@ -38,6 +38,7 @@
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
@ -115,7 +116,7 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca
// //
// Also note that this function *does not* dereference the remote account that the signature key is associated with. // 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. // 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, bool, error) { func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*url.URL, gtserror.WithCode) {
l := logrus.WithField("func", "AuthenticateFederatedRequest") l := logrus.WithField("func", "AuthenticateFederatedRequest")
var publicKey interface{} var publicKey interface{}
@ -125,33 +126,43 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// thanks to signaturecheck.go in the security package, we should already have a signature verifier set on the context // thanks to signaturecheck.go in the security package, we should already have a signature verifier set on the context
vi := ctx.Value(ap.ContextRequestingPublicKeyVerifier) vi := ctx.Value(ap.ContextRequestingPublicKeyVerifier)
if vi == nil { if vi == nil {
l.Debug("request wasn't signed") err := errors.New("http request wasn't signed or http signature was invalid")
return nil, false, nil // request wasn't signed errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error())
l.Debug(errWithCode)
return nil, errWithCode
} }
verifier, ok := vi.(httpsig.Verifier) verifier, ok := vi.(httpsig.Verifier)
if !ok { if !ok {
l.Debug("couldn't extract sig verifier") err := errors.New("http request wasn't signed or http signature was invalid")
return nil, false, nil // couldn't extract the verifier errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error())
l.Debug(errWithCode)
return nil, errWithCode
} }
// we should have the signature itself set too // we should have the signature itself set too
si := ctx.Value(ap.ContextRequestingPublicKeySignature) si := ctx.Value(ap.ContextRequestingPublicKeySignature)
if vi == nil { if si == nil {
l.Debug("request wasn't signed") err := errors.New("http request wasn't signed or http signature was invalid")
return nil, false, nil // request wasn't signed errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error())
l.Debug(errWithCode)
return nil, errWithCode
} }
signature, ok := si.(string) signature, ok := si.(string)
if !ok { if !ok {
l.Debug("couldn't extract signature") err := errors.New("http request wasn't signed or http signature was invalid")
return nil, false, nil // couldn't extract the signature errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error())
l.Debug(errWithCode)
return nil, errWithCode
} }
// now figure out who actually signed it
requestingPublicKeyID, err := url.Parse(verifier.KeyId()) requestingPublicKeyID, err := url.Parse(verifier.KeyId())
if err != nil { if err != nil {
l.Debug("couldn't parse public key URL") errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key URL %s", verifier.KeyId()))
return nil, false, err // couldn't parse the public key ID url l.Debug(errWithCode)
return nil, errWithCode
} }
requestingRemoteAccount := &gtsmodel.Account{} requestingRemoteAccount := &gtsmodel.Account{}
@ -163,12 +174,16 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing // the request is coming from INSIDE THE HOUSE so skip the remote dereferencing
l.Tracef("proceeding without dereference for local public key %s", requestingPublicKeyID) l.Tracef("proceeding without dereference for local public key %s", requestingPublicKeyID)
if err := f.db.GetWhere(ctx, []db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil { if err := f.db.GetWhere(ctx, []db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil {
return nil, false, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err) errWithCode := gtserror.NewErrorInternalError(fmt.Errorf("couldn't get account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err))
l.Debug(errWithCode)
return nil, errWithCode
} }
publicKey = requestingLocalAccount.PublicKey publicKey = requestingLocalAccount.PublicKey
pkOwnerURI, err = url.Parse(requestingLocalAccount.URI) pkOwnerURI, err = url.Parse(requestingLocalAccount.URI)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err) errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key owner URL %s", requestingLocalAccount.URI))
l.Debug(errWithCode)
return nil, errWithCode
} }
} else if err := f.db.GetWhere(ctx, []db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingRemoteAccount); err == nil { } else if err := f.db.GetWhere(ctx, []db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingRemoteAccount); err == nil {
// REMOTE ACCOUNT REQUEST WITH KEY CACHED LOCALLY // REMOTE ACCOUNT REQUEST WITH KEY CACHED LOCALLY
@ -177,7 +192,9 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
publicKey = requestingRemoteAccount.PublicKey publicKey = requestingRemoteAccount.PublicKey
pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI) pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("error parsing url %s: %s", requestingRemoteAccount.URI, err) errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key owner URL %s", requestingRemoteAccount.URI))
l.Debug(errWithCode)
return nil, errWithCode
} }
} else { } else {
// REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY // REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY
@ -186,56 +203,72 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
l.Tracef("proceeding with dereference for uncached public key %s", requestingPublicKeyID) l.Tracef("proceeding with dereference for uncached public key %s", requestingPublicKeyID)
transport, err := f.transportController.NewTransportForUsername(ctx, requestedUsername) transport, err := f.transportController.NewTransportForUsername(ctx, requestedUsername)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("transport err: %s", err) errWithCode := gtserror.NewErrorInternalError(fmt.Errorf("error creating transport for %s: %s", requestedUsername, err))
l.Debug(errWithCode)
return nil, errWithCode
} }
// The actual http call to the remote server is made right here in the Dereference function. // The actual http call to the remote server is made right here in the Dereference function.
b, err := transport.Dereference(ctx, requestingPublicKeyID) b, err := transport.Dereference(ctx, requestingPublicKeyID)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err) errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err))
l.Debug(errWithCode)
return nil, errWithCode
} }
// if the key isn't in the response, we can't authenticate the request // if the key isn't in the response, we can't authenticate the request
requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID) requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err) errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err))
l.Debug(errWithCode)
return nil, errWithCode
} }
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
return nil, false, errors.New("publicKeyPem property is not provided or it is not embedded as a value") errWithCode := gtserror.NewErrorNotAuthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value"))
l.Debug(errWithCode)
return nil, errWithCode
} }
// and decode the PEM so that we can parse it as a golang public key // and decode the PEM so that we can parse it as a golang public key
pubKeyPem := pkPemProp.Get() pubKeyPem := pkPemProp.Get()
block, _ := pem.Decode([]byte(pubKeyPem)) block, _ := pem.Decode([]byte(pubKeyPem))
if block == nil || block.Type != "PUBLIC KEY" { if block == nil || block.Type != "PUBLIC KEY" {
return nil, false, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type") errWithCode := gtserror.NewErrorNotAuthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type"))
l.Debug(errWithCode)
return nil, errWithCode
} }
publicKey, err = x509.ParsePKIXPublicKey(block.Bytes) publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("could not parse public key from block bytes: %s", err) errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err))
l.Debug(errWithCode)
return nil, errWithCode
} }
// all good! we just need the URI of the key owner to return // all good! we just need the URI of the key owner to return
pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
return nil, false, errors.New("publicKeyOwner property is not provided or it is not embedded as a value") errWithCode := gtserror.NewErrorNotAuthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value"))
l.Debug(errWithCode)
return nil, errWithCode
} }
pkOwnerURI = pkOwnerProp.GetIRI() pkOwnerURI = pkOwnerProp.GetIRI()
} }
// after all that, public key should be defined // after all that, public key should be defined
if publicKey == nil { if publicKey == nil {
return nil, false, errors.New("returned public key was empty") errWithCode := gtserror.NewErrorInternalError(errors.New("returned public key was empty"))
l.Debug(errWithCode)
return nil, errWithCode
} }
// do the actual authentication here! // do the actual authentication here!
algos := []httpsig.Algorithm{ algos := []httpsig.Algorithm{
httpsig.RSA_SHA512,
httpsig.RSA_SHA256, httpsig.RSA_SHA256,
httpsig.RSA_SHA512,
httpsig.ED25519, httpsig.ED25519,
} }
@ -244,11 +277,12 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
err := verifier.Verify(publicKey, algo) err := verifier.Verify(publicKey, algo)
if err == nil { if err == nil {
l.Tracef("authentication for %s PASSED with algorithm %s", pkOwnerURI, algo) l.Tracef("authentication for %s PASSED with algorithm %s", pkOwnerURI, algo)
return pkOwnerURI, true, nil return pkOwnerURI, nil
} }
l.Tracef("authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err) l.Tracef("authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err)
} }
l.Infof("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature) errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature))
return nil, false, nil l.Debug(errWithCode)
return nil, errWithCode
} }

View file

@ -119,15 +119,17 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
return nil, false, fmt.Errorf("could not fetch receiving account with username %s: %s", username, err) return nil, false, fmt.Errorf("could not fetch receiving account with username %s: %s", username, err)
} }
publicKeyOwnerURI, authenticated, err := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username) publicKeyOwnerURI, errWithCode := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username)
if err != nil { if errWithCode != nil {
l.Debugf("request not authenticated: %s", err) switch errWithCode.Code() {
return ctx, false, err case http.StatusUnauthorized, http.StatusForbidden, http.StatusBadRequest:
} // if 400, 401, or 403, obey the interface by writing the header and bailing
w.WriteHeader(errWithCode.Code())
if !authenticated { return ctx, false, nil
w.WriteHeader(http.StatusForbidden) default:
return ctx, false, nil // if not, there's been a proper error
return ctx, false, err
}
} }
// authentication has passed, so add an instance entry for this instance if it hasn't been done already // authentication has passed, so add an instance entry for this instance if it hasn't been done already

View file

@ -27,6 +27,7 @@
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/transport"
@ -50,7 +51,7 @@ type Federator interface {
// If the request does not pass authentication, or there's a domain block, nil, false, nil will be returned. // If the request does not pass authentication, or there's a domain block, nil, false, nil will be returned.
// //
// If something goes wrong during authentication, nil, false, and an error will be returned. // If something goes wrong during authentication, nil, false, and an error will be returned.
AuthenticateFederatedRequest(ctx context.Context, username string) (*url.URL, bool, error) AuthenticateFederatedRequest(ctx context.Context, username string) (*url.URL, gtserror.WithCode)
// FingerRemoteAccount performs a webfinger lookup for a remote account, using the .well-known path. It will return the ActivityPub URI for that // FingerRemoteAccount performs a webfinger lookup for a remote account, using the .well-known path. It will return the ActivityPub URI for that
// account, or an error if it doesn't exist or can't be retrieved. // account, or an error if it doesn't exist or can't be retrieved.

View file

@ -20,7 +20,6 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
@ -36,9 +35,9 @@ func (p *processor) GetFollowers(ctx context.Context, requestedUsername string,
} }
// authenticate the request // authenticate the request
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated { if errWithCode != nil {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") return nil, errWithCode
} }
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)

View file

@ -20,7 +20,6 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
@ -36,9 +35,9 @@ func (p *processor) GetFollowing(ctx context.Context, requestedUsername string,
} }
// authenticate the request // authenticate the request
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated { if errWithCode != nil {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") return nil, errWithCode
} }
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)

View file

@ -20,7 +20,6 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
@ -37,9 +36,9 @@ func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, pag
} }
// authenticate the request // authenticate the request
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated { if errWithCode != nil {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") return nil, errWithCode
} }
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)

View file

@ -20,7 +20,6 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
@ -38,9 +37,9 @@ func (p *processor) GetStatus(ctx context.Context, requestedUsername string, req
} }
// authenticate the request // authenticate the request
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated { if errWithCode != nil {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") return nil, errWithCode
} }
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)

View file

@ -20,7 +20,6 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
@ -38,9 +37,9 @@ func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername stri
} }
// authenticate the request // authenticate the request
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated { if errWithCode != nil {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") return nil, errWithCode
} }
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)

View file

@ -20,7 +20,6 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
@ -46,13 +45,9 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
} }
} else { } else {
// if it's any other path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile // if it's any other path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil { if errWithCode != nil {
return nil, gtserror.NewErrorNotAuthorized(err, "not authorized") return nil, errWithCode
}
if !authenticated {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
} }
// if we're not already handshaking/dereferencing a remote account, dereference it now // if we're not already handshaking/dereferencing a remote account, dereference it now