2021-05-08 14:25:55 +02:00
/ *
GoToSocial
Copyright ( C ) 2021 GoToSocial Authors admin @ gotosocial . org
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU Affero General Public License for more details .
You should have received a copy of the GNU Affero General Public License
along with this program . If not , see < http : //www.gnu.org/licenses/>.
* /
package federation
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/go-fed/httpsig"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2021-05-17 19:06:58 +02:00
"github.com/superseriousbusiness/gotosocial/internal/transport"
2021-05-08 14:25:55 +02:00
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
/ *
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
}
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 http signature.
//
// Authenticate in this case is defined as just 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. This function
// *does not* check whether the request is authorized, only whether it's authentic.
//
// 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.
//
// 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 ( username string , r * http . Request ) ( * url . URL , error ) {
2021-05-09 20:34:27 +02:00
// set this extra field for signature validation
r . Header . Set ( "host" , f . config . Host )
2021-05-08 14:25:55 +02:00
verifier , err := httpsig . NewVerifier ( r )
if err != nil {
return nil , fmt . Errorf ( "could not create http sig verifier: %s" , err )
}
// The key ID should be given in the signature so that we know where to fetch it from the remote server.
// This will be something like https://example.org/users/whatever_requesting_user#main-key
requestingPublicKeyID , err := url . Parse ( verifier . KeyId ( ) )
if err != nil {
return nil , fmt . Errorf ( "could not parse key id into a url: %s" , err )
}
transport , err := f . GetTransportForUser ( username )
if err != nil {
return nil , fmt . Errorf ( "transport err: %s" , err )
}
// The actual http call to the remote server is made right here in the Dereference function.
b , err := transport . Dereference ( context . Background ( ) , requestingPublicKeyID )
if err != nil {
return nil , fmt . Errorf ( "error deferencing key %s: %s" , requestingPublicKeyID . String ( ) , err )
}
// if the key isn't in the response, we can't authenticate the request
requestingPublicKey , err := getPublicKeyFromResponse ( context . Background ( ) , b , requestingPublicKeyID )
if err != nil {
return nil , fmt . Errorf ( "error getting key %s from response %s: %s" , requestingPublicKeyID . String ( ) , string ( b ) , err )
}
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
pkPemProp := requestingPublicKey . GetW3IDSecurityV1PublicKeyPem ( )
if pkPemProp == nil || ! pkPemProp . IsXMLSchemaString ( ) {
return nil , errors . New ( "publicKeyPem property is not provided or it is not embedded as a value" )
}
// 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" {
return nil , errors . New ( "could not decode publicKeyPem to PUBLIC KEY pem block type" )
}
p , err := x509 . ParsePKIXPublicKey ( block . Bytes )
if err != nil {
return nil , fmt . Errorf ( "could not parse public key from block bytes: %s" , err )
}
if p == nil {
return nil , errors . New ( "returned public key was empty" )
}
// do the actual authentication here!
algo := httpsig . RSA_SHA256 // TODO: make this more robust
if err := verifier . Verify ( p , algo ) ; err != nil {
return nil , fmt . Errorf ( "error verifying key %s: %s" , requestingPublicKeyID . String ( ) , err )
}
// all good! we just need the URI of the key owner to return
pkOwnerProp := requestingPublicKey . GetW3IDSecurityV1Owner ( )
if pkOwnerProp == nil || ! pkOwnerProp . IsIRI ( ) {
return nil , errors . New ( "publicKeyOwner property is not provided or it is not embedded as a value" )
}
pkOwnerURI := pkOwnerProp . GetIRI ( )
return pkOwnerURI , nil
}
func ( f * federator ) DereferenceRemoteAccount ( username string , remoteAccountID * url . URL ) ( typeutils . Accountable , error ) {
transport , err := f . GetTransportForUser ( username )
if err != nil {
return nil , fmt . Errorf ( "transport err: %s" , err )
}
b , err := transport . Dereference ( context . Background ( ) , remoteAccountID )
if err != nil {
return nil , fmt . Errorf ( "error deferencing %s: %s" , remoteAccountID . String ( ) , err )
}
m := make ( map [ string ] interface { } )
if err := json . Unmarshal ( b , & m ) ; err != nil {
return nil , fmt . Errorf ( "error unmarshalling bytes into json: %s" , err )
}
t , err := streams . ToType ( context . Background ( ) , m )
if err != nil {
return nil , fmt . Errorf ( "error resolving json into ap vocab type: %s" , err )
}
switch t . GetTypeName ( ) {
case string ( gtsmodel . ActivityStreamsPerson ) :
p , ok := t . ( vocab . ActivityStreamsPerson )
if ! ok {
return nil , errors . New ( "error resolving type as activitystreams person" )
}
return p , nil
case string ( gtsmodel . ActivityStreamsApplication ) :
2021-05-09 20:34:27 +02:00
p , ok := t . ( vocab . ActivityStreamsApplication )
if ! ok {
return nil , errors . New ( "error resolving type as activitystreams application" )
}
return p , nil
2021-05-08 14:25:55 +02:00
}
return nil , fmt . Errorf ( "type name %s not supported" , t . GetTypeName ( ) )
}
2021-05-17 19:06:58 +02:00
func ( f * federator ) GetTransportForUser ( username string ) ( transport . Transport , error ) {
2021-05-08 14:25:55 +02:00
// We need an account to use to create a transport for dereferecing the signature.
// If a username has been given, we can fetch the account with that username and use it.
// Otherwise, we can take the instance account and use those credentials to make the request.
ourAccount := & gtsmodel . Account { }
var u string
if username == "" {
u = f . config . Host
} else {
u = username
}
if err := f . db . GetLocalAccountByUsername ( u , ourAccount ) ; err != nil {
return nil , fmt . Errorf ( "error getting account %s from db: %s" , username , err )
}
transport , err := f . transportController . NewTransport ( ourAccount . PublicKeyURI , ourAccount . PrivateKey )
if err != nil {
return nil , fmt . Errorf ( "error creating transport for user %s: %s" , username , err )
}
return transport , nil
}