forked from grumpydevelop/federator
		
	added proper application-signing
- we now check whether the request was sent from contentnation and approve the request with contentnations pub key - fix issue where inbox wasn't gotten as it also got a username - rough initial draft of parsing of connector-specific data (plugins/federator/contentnation::jsonToActivity) - added key paths and headername to config
This commit is contained in:
		
							parent
							
								
									0edc4c0db6
								
							
						
					
					
						commit
						352377887c
					
				
					 10 changed files with 147 additions and 29 deletions
				
			
		
							
								
								
									
										11
									
								
								config.ini
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								config.ini
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -13,10 +13,15 @@ compiledir = '../cache'
 | 
			
		|||
 | 
			
		||||
[plugins]
 | 
			
		||||
rediscache = 'rediscache.php'
 | 
			
		||||
dummy = 'dummyconnector.php'
 | 
			
		||||
# dummy = 'dummyconnector.php'
 | 
			
		||||
contentnation = 'contentnation.php'
 | 
			
		||||
mastodon = 'mastodon.php'
 | 
			
		||||
 | 
			
		||||
[maintenance]
 | 
			
		||||
username = 'federatoradmin'
 | 
			
		||||
password = '*change*me*as*well'
 | 
			
		||||
password = '*change*me*as*well'
 | 
			
		||||
 | 
			
		||||
[keys]
 | 
			
		||||
headerSenderName = 'contentnation'
 | 
			
		||||
contentnationPublicKeyPath = '../contentnation.pub'
 | 
			
		||||
federatorPrivateKeyPath = '../federator.key'
 | 
			
		||||
federatorPublicKeyPath = '../federator.pub'
 | 
			
		||||
							
								
								
									
										14
									
								
								contentnation.pub
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								contentnation.pub
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
-----BEGIN PUBLIC KEY-----
 | 
			
		||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArTNHQL76ZuM7meWvtfHC
 | 
			
		||||
DOAivi9D4m2u3JdgN2R/cMa4+U4jURVJ1BZBesVkW8bS7AhBpFjAOUSNDsvsB7Yf
 | 
			
		||||
mxUa8vKD7GgTLQPUhP10EeEZz+R/onDlTU7TCDVd1PDdQwlx/2aT+m7K2KwmnOC5
 | 
			
		||||
ZUO0jO7EtXn4qhA1qt8oRFRogQlzvMbLr6lYhkomBxn4XqezbtDw+HQjJ2Af5ECR
 | 
			
		||||
+tElbkEZWpEG/fwJvv1hhqqDToloOkK2YhMTiOZFesafH+AFQq1pYx6hFoa6TIkG
 | 
			
		||||
8aLoLuPY+IBZqXtPLyh1cIiBYqAiyo0lIHjtKjWnPbXhu83EZ3VOvpbopYonSCOX
 | 
			
		||||
0uHsbf1fn9NGhe5TSSxbz0SAGJgZTr2VvHinqZ0k3me4CS/HUzkvOtMdVtwJdqLp
 | 
			
		||||
N/pUfGRjeiDbO5JvOMrimUP3klVB54Nf0IIw7aMhD3yO7KGoxRIV89H6i5TJF3zY
 | 
			
		||||
WeirON6ejHapNw6WCWL7YY5WDsGuiMIuAcfwYAcsaqaKYktjqJZT1hejJNVqLhSU
 | 
			
		||||
ZaVBcl56/VO9lPoC8u7NXFfnT4h3bIfL8Ft3riabQzFSMXjLI2Q27BG7R5xNEo+u
 | 
			
		||||
aG2STkjKam/+q5VeUnpg1F0HBz/QuZ+GcsItD8uN+IjN9yZhJjryRwhn2KcaPIYz
 | 
			
		||||
upOsIXACYAm0kfqNRK5qdwECAwEAAQ==
 | 
			
		||||
-----END PUBLIC KEY-----
 | 
			
		||||
							
								
								
									
										14
									
								
								federator.pub
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								federator.pub
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
-----BEGIN PUBLIC KEY-----
 | 
			
		||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6kZOjPKQjQIo5dugu80e
 | 
			
		||||
gsQXPkWhGjSUgbJ5UNwcyFto4p2euhVqVnTVOCWeS9+dPQP14fuVowODceaZLmGg
 | 
			
		||||
sBqraBZ4FNb76ByBdfiqDmPvUP61hrdDCZ52IPMYq7e3knWVakGouSqyoa/TVf3l
 | 
			
		||||
5oa7qgYnRDvHQXkA51Dj/1BqW57WeBQzEd5nwFhhAKZuVLxC/+xEu6Ohf+6WC2qR
 | 
			
		||||
Dz/toI26A3QrMCgmt21ELxjTyNmUdTL6U8PjutiMZJ2sy5uhR7stRNzoWt0AnRJE
 | 
			
		||||
1NlwPU8tKpfXAv00zxTS4xuLt0zv2lNSSRfECeM2g86fXuhMB0NYd30Mgda+Svbu
 | 
			
		||||
MEFvOkB5xEAi1NRamETV9Ci/LBqShC1ZBcY5QdikH4S0awIsQA3YMsK0y4+gCY1S
 | 
			
		||||
oHwFjR+KhiGKBa4NaKsfFy3JL5OB6+8PF6z2ICbD26X1jJy9ScLHrljd/AKVNtXE
 | 
			
		||||
Jaz2NDrqmqdjCILxROTle5aNnOfpaAMmiszIWmZNuCWRBbrpVXPeOR3D+qLEld3u
 | 
			
		||||
z2l/i2ywfNtt0VrMhKMWjT99aPOHyMvInuZGYx2RVhzYyf5h3V6FCoD67ihInbCa
 | 
			
		||||
SfDGHKhEa6gQaIIZi2EfY2QbYbZG/4gX9BHfUlTYMoFgW5P2qS1c27tTi/1LkJKx
 | 
			
		||||
CZWiL/7VWZ/nx94SQPL76k0CAwEAAQ==
 | 
			
		||||
-----END PUBLIC KEY-----
 | 
			
		||||
| 
						 | 
				
			
			@ -195,12 +195,11 @@ class Api extends Main
 | 
			
		|||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
     *
 | 
			
		||||
     * @param string[] $headers
 | 
			
		||||
     *          permission(s) to check for
 | 
			
		||||
     * @param string[] $headers the headers
 | 
			
		||||
     * @throws Exceptions\PermissionDenied
 | 
			
		||||
     * @return string|Exceptions\PermissionDenied
 | 
			
		||||
     */
 | 
			
		||||
    public static function checkSignature($headers)
 | 
			
		||||
    public function checkSignature($headers)
 | 
			
		||||
    {
 | 
			
		||||
        $signatureHeader = $headers['Signature'] ?? null;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -209,33 +208,40 @@ class Api extends Main
 | 
			
		|||
            throw new Exceptions\PermissionDenied("Missing Signature header");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $config = $this->getConfig();
 | 
			
		||||
 | 
			
		||||
        // Parse Signature header
 | 
			
		||||
        preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
 | 
			
		||||
        $signatureParts = array_combine($matches[1], $matches[2]);
 | 
			
		||||
 | 
			
		||||
        $signature = base64_decode($signatureParts['signature']);
 | 
			
		||||
        $keyId = $signatureParts['keyId'];
 | 
			
		||||
        $signedHeaders = explode(' ', $signatureParts['headers']);
 | 
			
		||||
        if (isset($headers['X-Sender']) && $headers['X-Sender'] === $config['keys']['headerSenderName']) {
 | 
			
		||||
            $pKeyPath = $_SERVER['DOCUMENT_ROOT'] . $config['keys']['contentnationPublicKeyPath'];
 | 
			
		||||
            $publicKeyPem = file_get_contents($pKeyPath);
 | 
			
		||||
        } else {
 | 
			
		||||
            $keyId = $signatureParts['keyId'];
 | 
			
		||||
 | 
			
		||||
        // Fetch public key from `keyId` (usually actor URL + #main-key)
 | 
			
		||||
        [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
 | 
			
		||||
            // Fetch public key from `keyId` (usually actor URL + #main-key)
 | 
			
		||||
            [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
 | 
			
		||||
 | 
			
		||||
        if ($info['http_code'] != 200) {
 | 
			
		||||
            http_response_code(500);
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId");
 | 
			
		||||
            if ($info['http_code'] != 200) {
 | 
			
		||||
                http_response_code(500);
 | 
			
		||||
                throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $actor = json_decode($publicKeyData, true);
 | 
			
		||||
 | 
			
		||||
            if (!is_array($actor) || !isset($actor['id'])) {
 | 
			
		||||
                throw new Exceptions\PermissionDenied("Invalid actor data");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $actor = json_decode($publicKeyData, true);
 | 
			
		||||
 | 
			
		||||
        if (!is_array($actor) || !isset($actor['id'])) {
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Invalid actor data");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!isset($publicKeyPem)) {
 | 
			
		||||
        if (!isset($publicKeyPem) || $publicKeyPem === false) {
 | 
			
		||||
            http_response_code(500);
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Invalid public key format from actor with keyId: $keyId");
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Public key couldn't be determined");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Reconstruct the signed string
 | 
			
		||||
| 
						 | 
				
			
			@ -251,6 +257,7 @@ class Api extends Main
 | 
			
		|||
 | 
			
		||||
            $signedString .= strtolower($header) . ": " . $headerValue . "\n";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $signedString = rtrim($signedString);
 | 
			
		||||
 | 
			
		||||
        // Verify the signature
 | 
			
		||||
| 
						 | 
				
			
			@ -261,12 +268,11 @@ class Api extends Main
 | 
			
		|||
        }
 | 
			
		||||
        if ($verified != 1) {
 | 
			
		||||
            http_response_code(500);
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Signature verification failed for publicKey with keyId: $keyId");
 | 
			
		||||
            throw new Exceptions\PermissionDenied("Signature verification failed for publicKey");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $actorId = $actor['id'];
 | 
			
		||||
        // Signature is valid!
 | 
			
		||||
        return "Signature verified from actor: " . $actorId;
 | 
			
		||||
        return "Signature verified.";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,6 +69,7 @@ class FedUsers implements APIInterface
 | 
			
		|||
                } else {
 | 
			
		||||
                    switch ($paths[1]) {
 | 
			
		||||
                        case 'inbox':
 | 
			
		||||
                            $_username = NULL;
 | 
			
		||||
                            $handler = new FedUsers\Inbox($this->main);
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,14 +106,18 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
			
		||||
        $input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
 | 
			
		||||
        if (!is_array($activity)) {
 | 
			
		||||
        if (!is_array($input)) {
 | 
			
		||||
            error_log("Outbox::post Input wasn't of type array");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $outboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
			
		||||
 | 
			
		||||
        if (isset($allHeaders['X-Sender']) && $allHeaders['X-Sender'] === $this->main->getConfig()['keys']['headerSenderName']) {
 | 
			
		||||
            $outboxActivity = $this->main->getConnector()->jsonToActivity($input);
 | 
			
		||||
        } else {
 | 
			
		||||
            $outboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($input);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($outboxActivity === false) {
 | 
			
		||||
            error_log("Outbox::post couldn't create outboxActivity");
 | 
			
		||||
| 
						 | 
				
			
			@ -142,9 +146,20 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (empty($users)) { // todo remove, debugging for now
 | 
			
		||||
            $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
 | 
			
		||||
            // Save the raw input and parsed JSON to a file for inspection
 | 
			
		||||
            file_put_contents(
 | 
			
		||||
                $rootDir . 'logs/outbox.log',
 | 
			
		||||
                date('Y-m-d H:i:s') . ": ==== POST Outbox Activity ====\n" . json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
			
		||||
                FILE_APPEND
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        if ($_user !== false && !in_array($_user, $users, true)) {
 | 
			
		||||
            $users[] = $_user;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($users as $user) {
 | 
			
		||||
            if (!isset($user)) {
 | 
			
		||||
                continue;
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +167,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
 | 
			
		||||
            $this->postForUser($user, $outboxActivity);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,4 +56,12 @@ interface Connector
 | 
			
		|||
     * @return \Federator\Data\Stats|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteStats();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string, mixed> $jsonData the json data from our platfrom
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -329,6 +329,39 @@ class ContentNation implements Connector
 | 
			
		|||
        }
 | 
			
		||||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string, mixed> $jsonData the json data from our platfrom
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData)
 | 
			
		||||
    {
 | 
			
		||||
        $ap = [
 | 
			
		||||
            '@context' => 'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
            'type' => 'Create',
 | 
			
		||||
            'id' => $jsonData['id'] ?? null,
 | 
			
		||||
            'actor' => $jsonData['actor']['id'] ?? null,
 | 
			
		||||
            'published' => $jsonData['object']['published'] ?? null,
 | 
			
		||||
            'to' => ['https://www.w3.org/ns/activitystreams#Public'],
 | 
			
		||||
            'cc' => [$jsonData['related']['cc']['followers'] ?? null],
 | 
			
		||||
            'object' => [
 | 
			
		||||
                'type' => 'Note',
 | 
			
		||||
                'id' => $jsonData['object']['id'] ?? null,
 | 
			
		||||
                'summary' => $jsonData['object']['summary'] ?? '',
 | 
			
		||||
                'content' => $jsonData['object']['content'] ?? '',
 | 
			
		||||
                'published' => $jsonData['object']['published'] ?? null,
 | 
			
		||||
                'attributedTo' => $jsonData['actor']['id'] ?? null,
 | 
			
		||||
                'to' => ['https://www.w3.org/ns/activitystreams#Public'],
 | 
			
		||||
                'cc' => [$jsonData['related']['cc']['followers'] ?? null],
 | 
			
		||||
                'url' => $jsonData['object']['url'] ?? null,
 | 
			
		||||
                'inReplyTo' => $jsonData['related']['article']['id'] ?? null,
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        return \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace Federator;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,6 +57,16 @@ class DummyConnector implements Connector
 | 
			
		|||
        return $stats;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get remote user by name
 | 
			
		||||
     * @param string $_name user or profile name
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,6 +90,17 @@ class RedisCache implements Cache
 | 
			
		|||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData) {
 | 
			
		||||
        error_log("rediscache::jsonToActivity not implemented");
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by given user
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue