[chore] Refactor AP authentication, other small bits of tidying up (#1874)

This commit is contained in:
tobi
2023-06-13 16:47:56 +02:00
committed by GitHub
parent 433b56d2f9
commit 24fbdf2b0a
38 changed files with 1280 additions and 996 deletions

View File

@ -19,298 +19,297 @@ package federation
import (
"context"
"crypto/x509"
"crypto/rsa"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"codeberg.org/gruf/go-kv"
"github.com/go-fed/httpsig"
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
)
/*
publicKeyer is BORROWED DIRECTLY FROM https://github.com/go-fed/apcore/blob/master/ap/util.go
Thank you @cj@mastodon.technology ! <3
*/
type publicKeyer interface {
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
}
/*
getPublicKeyFromResponse is adapted from https://github.com/go-fed/apcore/blob/master/ap/util.go
Thank you @cj@mastodon.technology ! <3
*/
func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (vocab.W3IDSecurityV1PublicKey, error) {
m := make(map[string]interface{})
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
var (
errUnsigned = errors.New("http request wasn't signed or http signature was invalid")
signingAlgorithms = []httpsig.Algorithm{
httpsig.RSA_SHA256, // Prefer common RSA_SHA256.
httpsig.RSA_SHA512, // Fall back to less common RSA_SHA512.
httpsig.ED25519, // Try ED25519 as a long shot.
}
)
t, err := streams.ToType(c, m)
if err != nil {
return nil, err
}
pker, ok := t.(publicKeyer)
if !ok {
return nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %T", t)
}
pkp := pker.GetW3IDSecurityV1PublicKey()
if pkp == nil {
return nil, errors.New("publicKey property is not provided")
}
var pkpFound vocab.W3IDSecurityV1PublicKey
for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() {
if !pkpIter.IsW3IDSecurityV1PublicKey() {
continue
}
pkValue := pkpIter.Get()
var pkID *url.URL
pkID, err = pub.GetId(pkValue)
if err != nil {
return nil, err
}
if pkID.String() != keyID.String() {
continue
}
pkpFound = pkValue
break
}
if pkpFound == nil {
return nil, fmt.Errorf("cannot find publicKey with id: %s", keyID)
}
return pkpFound, nil
}
// 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.
// 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.
//
// 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 key from the signature and checking it against the remote public key.
// '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
// key from the signature and checking it against the remote public key.
//
// The provided username will be used to generate a transport for making remote requests/derefencing the public key ID of the request signature.
// Ideally you should pass in the username of the user *being requested*, so that the remote server can decide how to handle the request based on who's making it.
// Ie., if the request on this server is for https://example.org/users/some_username then you should pass in the username 'some_username'.
// The remote server will then know that this is the user making the dereferencing request, and they can decide to allow or deny the request depending on their settings.
// The provided username will be used to generate a transport for making remote
// requests/derefencing the public key ID of the request signature. Ideally you
// should pass in the username of the user *being requested*, so that the remote
// server can decide how to handle the request based on who's making it. Ie., if
// the request on this server is for https://example.org/users/some_username then
// you should pass in the username 'some_username'. The remote server will then
// know that this is the user making the dereferencing request, and they can decide
// to allow or deny the request depending on their settings.
//
// 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.
// 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.
//
// 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.
// 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) {
var publicKey interface{}
var pkOwnerURI *url.URL
var err error
// thanks to signaturecheck.go in the security package, we should already have a signature verifier set on the context
vi := ctx.Value(ap.ContextRequestingPublicKeyVerifier)
if vi == nil {
err := errors.New("http request wasn't signed or http signature was invalid")
errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
log.Debug(ctx, errWithCode)
// Thanks to the signature check middleware,
// we should already have an http signature
// verifier set on the context. If we don't,
// this is an unsigned request.
verifier := gtscontext.HTTPSignatureVerifier(ctx)
if verifier == nil {
err := gtserror.Newf("%w", errUnsigned)
errWithCode := gtserror.NewErrorUnauthorized(err, errUnsigned.Error(), "(verifier)")
return nil, errWithCode
}
verifier, ok := vi.(httpsig.Verifier)
if !ok {
err := errors.New("http request wasn't signed or http signature was invalid")
errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
log.Debug(ctx, errWithCode)
// We should have the signature itself set too.
signature := gtscontext.HTTPSignature(ctx)
if signature == "" {
err := gtserror.Newf("%w", errUnsigned)
errWithCode := gtserror.NewErrorUnauthorized(err, errUnsigned.Error(), "(signature)")
return nil, errWithCode
}
// we should have the signature itself set too
si := ctx.Value(ap.ContextRequestingPublicKeySignature)
if si == nil {
err := errors.New("http request wasn't signed or http signature was invalid")
errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
log.Debug(ctx, errWithCode)
// And finally the public key ID URI.
pubKeyID := gtscontext.HTTPSignaturePubKeyID(ctx)
if pubKeyID == nil {
err := gtserror.Newf("%w", errUnsigned)
errWithCode := gtserror.NewErrorUnauthorized(err, errUnsigned.Error(), "(pubKeyID)")
return nil, errWithCode
}
signature, ok := si.(string)
if !ok {
err := errors.New("http request wasn't signed or http signature was invalid")
errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
// now figure out who actually signed it
requestingPublicKeyID, err := url.Parse(verifier.KeyId())
if err != nil {
errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key URL %s", verifier.KeyId()))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
// At this point we know the request was signed,
// so now we need to validate the signature.
var (
requestingLocalAccount *gtsmodel.Account
requestingRemoteAccount *gtsmodel.Account
requestingHost = requestingPublicKeyID.Host
pubKeyIDStr = pubKeyID.String()
requestingAccountURI *url.URL
pubKey interface{}
errWithCode gtserror.WithCode
)
if host := config.GetHost(); strings.EqualFold(requestingHost, host) {
// LOCAL ACCOUNT REQUEST
// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing
log.Tracef(ctx, "proceeding without dereference for local public key %s", requestingPublicKeyID)
l := log.
WithContext(ctx).
WithFields(kv.Fields{
{"requestedUsername", requestedUsername},
{"pubKeyID", pubKeyIDStr},
}...)
requestingLocalAccount, err = f.db.GetAccountByPubkeyID(ctx, requestingPublicKeyID.String())
if err != nil {
errWithCode := gtserror.NewErrorInternalError(fmt.Errorf("couldn't get account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
publicKey = requestingLocalAccount.PublicKey
pkOwnerURI, err = url.Parse(requestingLocalAccount.URI)
if err != nil {
errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key owner URL %s", requestingLocalAccount.URI))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
} else if requestingRemoteAccount, err = f.db.GetAccountByPubkeyID(ctx, requestingPublicKeyID.String()); err == nil {
// REMOTE ACCOUNT REQUEST WITH KEY CACHED LOCALLY
// this is a remote account and we already have the public key for it so use that
log.Tracef(ctx, "proceeding without dereference for cached public key %s", requestingPublicKeyID)
publicKey = requestingRemoteAccount.PublicKey
pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI)
if err != nil {
errWithCode := gtserror.NewErrorBadRequest(err, fmt.Sprintf("couldn't parse public key owner URL %s", requestingRemoteAccount.URI))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
if pubKeyID.Host == config.GetHost() {
l.Trace("public key is ours, no dereference needed")
requestingAccountURI, pubKey, errWithCode = f.derefDBOnly(ctx, pubKeyIDStr)
} else {
// REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY
// the request is remote and we don't have the public key yet,
// so we need to authenticate the request properly by dereferencing the remote key
gone, err := f.CheckGone(ctx, requestingPublicKeyID)
if err != nil {
errWithCode := gtserror.NewErrorInternalError(fmt.Errorf("error checking for tombstone for %s: %s", requestingPublicKeyID, err))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
if gone {
errWithCode := gtserror.NewErrorGone(fmt.Errorf("account with public key %s is gone", requestingPublicKeyID))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
log.Tracef(ctx, "proceeding with dereference for uncached public key %s", requestingPublicKeyID)
trans, err := f.transportController.NewTransportForUsername(gtscontext.SetFastFail(ctx), requestedUsername)
if err != nil {
errWithCode := gtserror.NewErrorInternalError(fmt.Errorf("error creating transport for %s: %s", requestedUsername, err))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
// The actual http call to the remote server is made right here in the Dereference function.
b, err := trans.Dereference(ctx, requestingPublicKeyID)
if err != nil {
if gtserror.StatusCode(err) == http.StatusGone {
// if we get a 410 error it means the account that owns this public key has been deleted;
// we should add a tombstone to our database so that we can avoid trying to deref it in future
if err := f.HandleGone(ctx, requestingPublicKeyID); err != nil {
errWithCode := gtserror.NewErrorInternalError(fmt.Errorf("error marking account with public key %s as gone: %s", requestingPublicKeyID, err))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
errWithCode := gtserror.NewErrorGone(fmt.Errorf("account with public key %s is gone", requestingPublicKeyID))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
// if the key isn't in the response, we can't authenticate the request
requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID)
if err != nil {
errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value"))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
// and decode the PEM so that we can parse it as a golang public key
pubKeyPem := pkPemProp.Get()
block, _ := pem.Decode([]byte(pubKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
errWithCode := gtserror.NewErrorUnauthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type"))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
// all good! we just need the URI of the key owner to return
pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value"))
log.Debug(ctx, errWithCode)
return nil, errWithCode
}
pkOwnerURI = pkOwnerProp.GetIRI()
l.Trace("public key is not ours, checking if we need to dereference")
requestingAccountURI, pubKey, errWithCode = f.deref(ctx, requestedUsername, pubKeyIDStr, pubKeyID)
}
// after all that, public key should be defined
if publicKey == nil {
errWithCode := gtserror.NewErrorInternalError(errors.New("returned public key was empty"))
log.Debug(ctx, errWithCode)
if errWithCode != nil {
return nil, errWithCode
}
// do the actual authentication here!
algos := []httpsig.Algorithm{
httpsig.RSA_SHA256,
httpsig.RSA_SHA512,
httpsig.ED25519,
// Ensure public key now defined.
if pubKey == nil {
err := gtserror.New("public key was nil")
return nil, gtserror.NewErrorInternalError(err)
}
for _, algo := range algos {
log.Tracef(ctx, "trying algo: %s", algo)
err := verifier.Verify(publicKey, algo)
// 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 {
log.Tracef(ctx, "authentication for %s PASSED with algorithm %s", pkOwnerURI, algo)
return pkOwnerURI, nil
l.Tracef("authentication PASSED with %s", algo)
return requestingAccountURI, nil
}
log.Tracef(ctx, "authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err)
l.Tracef("authentication NOT PASSED with %s: %q", algo, err)
}
errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature))
log.Debug(ctx, errWithCode)
return nil, errWithCode
// At this point no algorithms passed.
err := gtserror.Newf(
"authentication NOT PASSED for public key %s; tried algorithms %+v; signature value was '%s'",
pubKeyIDStr, signature, signingAlgorithms,
)
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(
ctx context.Context,
pubKeyIDStr string,
) (*url.URL, interface{}, gtserror.WithCode) {
reqAcct, err := f.db.GetAccountByPubkeyID(ctx, pubKeyIDStr)
if err != nil {
err = gtserror.Newf("db error getting account with pubKeyID %s: %w", pubKeyIDStr, err)
return nil, nil, gtserror.NewErrorInternalError(err)
}
reqAcctURI, err := url.Parse(reqAcct.URI)
if err != nil {
err = gtserror.Newf("error parsing account uri with pubKeyID %s: %w", pubKeyIDStr, err)
return nil, nil, gtserror.NewErrorInternalError(err)
}
return reqAcctURI, reqAcct.PublicKey, 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(
ctx context.Context,
requestedUsername string,
pubKeyIDStr string,
pubKeyID *url.URL,
) (*url.URL, interface{}, gtserror.WithCode) {
l := log.
WithContext(ctx).
WithFields(kv.Fields{
{"requestedUsername", requestedUsername},
{"pubKeyID", pubKeyIDStr},
}...)
// 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
}
l.Trace("public key not cached, trying dereference")
// 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)
}
if gone {
err := gtserror.Newf("account with public key %s is gone", pubKeyIDStr)
return nil, nil, gtserror.NewErrorGone(err)
}
// Make an http call to get the pubkey.
pubKeyBytes, errWithCode := f.callForPubKey(ctx, requestedUsername, pubKeyID)
if errWithCode != nil {
return nil, 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)
}
return pubKeyOwner, pubKey, nil
}
// callForPubKey handles the nitty gritty of actually
// making a request for the given pubKeyID with a
// transport created on behalf of requestedUsername.
func (f *federator) callForPubKey(
ctx context.Context,
requestedUsername string,
pubKeyID *url.URL,
) ([]byte, gtserror.WithCode) {
// Use a transport to dereference the remote.
trans, err := f.transportController.NewTransportForUsername(
// We're on a hot path: don't retry if req fails.
gtscontext.SetFastFail(ctx),
requestedUsername,
)
if err != nil {
err = gtserror.Newf("error creating transport for %s: %w", requestedUsername, err)
return nil, gtserror.NewErrorInternalError(err)
}
// The actual http call to the remote server is
// made right here by the Dereference function.
pubKeyBytes, err := trans.Dereference(ctx, pubKeyID)
if err == nil {
// No problem.
return pubKeyBytes, nil
}
if gtserror.StatusCode(err) == http.StatusGone {
// 410 indicates remote public key no longer exists
// (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)
return nil, gtserror.NewErrorInternalError(err)
}
err := gtserror.Newf("account with public key %s is gone", pubKeyID)
return nil, gtserror.NewErrorGone(err)
}
// Fall back to generic error.
err = gtserror.Newf("error dereferencing public key %s: %w", pubKeyID, err)
return nil, gtserror.NewErrorInternalError(err)
}
// parsePubKeyBytes extracts an rsa public key from the
// given pubKeyBytes by trying to parse the pubKeyBytes
// as an ActivityPub type. It will return the public key
// itself, and the URI of the public key owner.
func parsePubKeyBytes(
ctx context.Context,
pubKeyBytes []byte,
pubKeyID *url.URL,
) (*rsa.PublicKey, *url.URL, error) {
m := make(map[string]interface{})
if err := json.Unmarshal(pubKeyBytes, &m); err != nil {
return nil, nil, err
}
t, err := streams.ToType(ctx, m)
if err != nil {
return nil, nil, err
}
withPublicKey, ok := t.(ap.WithPublicKey)
if !ok {
err = gtserror.Newf("resource at %s with type %T could not be converted to ap.WithPublicKey", pubKeyID, t)
return nil, nil, err
}
pubKey, _, pubKeyOwnerID, err := ap.ExtractPublicKey(withPublicKey)
if err != nil {
err = gtserror.Newf("resource at %s with type %T did not contain recognizable public key", pubKeyID, t)
return nil, nil, err
}
return pubKey, pubKeyOwnerID, nil
}