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