activitypub-1.0.x-dev/src/Services/ActivityPubSignature.php
src/Services/ActivityPubSignature.php
<?php
namespace Drupal\activitypub\Services;
use ActivityPhp\Server;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use phpseclib3\Crypt\RSA;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
class ActivityPubSignature implements ActivityPubSignatureInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* ActivityPubUtility constructor
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Psr\Log\LoggerInterface $logger
* The logger
*/
public function __construct(ConfigFactoryInterface $config_factory, FileSystemInterface $file_system, LoggerInterface $logger) {
$this->configFactory = $config_factory;
$this->fileSystem = $file_system;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function getPublicKey($path) {
$key_path = rtrim(activitypub_keys_path(), '/') . '/' . $path . '/public.pem';
return file_get_contents($key_path);
}
/**
* {@inheritdoc}
*/
public function getPrivateKey($path) {
$key_path = rtrim(activitypub_keys_path(), '/') . '/' . $path . '/private.pem';
return file_get_contents($key_path);
}
/**
* {@inheritdoc}
*/
public function createSignature($private_key_path, $host, $path, $digest, $date = NULL) {
if (!isset($date)) {
$date = gmdate('D, d M Y H:i:s T', time());
}
try {
$plaintext = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: $digest";
$rsa = RSA::loadPrivateKey($this->getPrivateKey($private_key_path)
)->withHash("sha256")->withPadding(RSA::SIGNATURE_PKCS1);
return $rsa->sign($plaintext);
}
catch (\Exception $e) {
$this->logger->error('Error creating the signature: @message', ['@message' => $e->getMessage()]);
return NULL;
}
}
/**
* {@inheritdoc}
*/
public function createDigest($message) {
return 'SHA-256=' . base64_encode(hash('sha256', $message, true));
}
/**
* {@inheritdoc}
*/
public function generateKeys($path) {
$return = FALSE;
try {
$private_key = RSA::createKey(4096);
$dir_path = rtrim(activitypub_keys_path(), '/') . '/' . $path;
$this->fileSystem->prepareDirectory($dir_path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
foreach (['public' => 'publickey', 'private' => 'privatekey'] as $filename => $rsakey) {
// Key uri.
$key_uri = "$dir_path/$filename.pem";
// Write key content to key file.
$this->fileSystem->saveData(($rsakey == "publickey") ? (string) $private_key->getPublicKey() : $private_key, $key_uri, FileSystemInterface::EXISTS_REPLACE);
// Set correct permission to key file.
$this->fileSystem->chmod($key_uri, 0600);
}
$return = TRUE;
}
catch (\Exception $e) {
$this->logger->error('Error generating keys: @message', ['@message' => $e->getMessage()]);
}
return $return;
}
/**
* {@inheritdoc}
*/
public function deleteKeys($path) {
$dir_path = rtrim(activitypub_keys_path(), '/') . '/' . $path;
$this->fileSystem->deleteRecursive($dir_path);
}
/**
* {@inheritdoc}
*/
public function verifySignature(Request $request, string $actor, Server $server) {
$verified = FALSE;
try {
$actor = $server->actor($actor);
$publicKeyPem = $actor->getPublicKeyPem();
$signature = '';
$strings = [];
$header_signature = $request->headers->get('signature');
$this->debug('request: ' . $request);
$this->debug("method:\n" . $request->getMethod());
$this->debug("path:\n" . $request->getPathInfo());
$this->debug("signature:\n" . $header_signature);
$sig_explode = explode(",", $header_signature ?: '');
$this->debug("sig explode:\n" . print_r($sig_explode, 1));
foreach ($sig_explode as $entry) {
$entry_ex = explode("=", $entry ?: '');
if (!empty($entry_ex[0]) && !empty($entry_ex[1])) {
if ($entry_ex[0] == 'headers') {
$ex = explode(" ", str_replace('"', '', $entry_ex[1]));
$this->debug("header explode: \n" . print_r($ex, 1));
foreach ($ex as $header_key) {
if ($header_key == '(request-target)') {
$parsed = parse_url($request->getRequestUri());
$strings[] = '(request-target): ' . strtolower($request->getMethod()) . ' ' . $parsed['path'];
}
else {
$strings[] = $header_key .': ' . $request->headers->get($header_key);
}
}
}
if ($entry_ex[0] == 'signature') {
$signature = str_replace(['"', 'signature='], '', $entry);
}
}
}
$this->debug("strings:\n" . print_r($strings, 1));
$this->debug("signature: \n" . print_r($signature, 1));
$data = implode("\n", $strings);
$this->debug("data:\n" . print_r($data, 1));
//'Signature' => 'keyId="' . $keyId . '#main-key",headers="(request-target) host date digest",signature="' . base64_encode($signature) . '",algorithm="rsa-sha256"',
//$plaintext = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: $digest";
if (!empty($data) && !empty($signature)) {
$rsa = RSA::loadPublicKey($publicKeyPem)
->withHash('sha256')
->withPadding(RSA::SIGNATURE_PKCS1);
$verified = $rsa->verify($data, base64_decode($signature, true));
}
}
catch (\Exception $e) {
$store = TRUE;
$message = $e->getMessage();
if ($this->configFactory->get('activitypub.settings')->get('log_ignore_error_signature')) {
foreach ($this->ignoreSignatureExceptionMessages() as $m) {
if (strpos($message, $m) !== FALSE) {
$store = FALSE;
break;
}
}
}
if ($store && $this->configFactory->get('activitypub.settings')->get('log_error_signature')) {
$this->logger->error('Signature verifying exception: @message', ['@message' => $message]);
}
}
return $verified;
}
/**
* Returns exceptions we'll ignore.
*
* @return string[]
*/
protected function ignoreSignatureExceptionMessages() {
return [
// Client error: `GET example.com` resulted in a `410 Gone` response: {"error":"Gone"}
'410 Gone',
// Client error: `GET example.com` resulted in a `404 Not Found` response:
'404 Not Found'
];
}
/**
* Debug helper function.
*
* @param $var
*/
protected function debug($var) {
//print_r($var);
//echo "<br /><br />-------------------------------<br /><br />";
}
}