From ea2a9eb42ae6e3f60e1b8cb4f6d4111bb5e26659 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 1 Aug 2024 21:47:04 -0600 Subject: [PATCH 1/4] Add Authorized Fetch --- app/Http/Controllers/Admin/AdminSettingsController.php | 2 ++ app/Services/AdminSettingsService.php | 1 + app/Services/ConfigCacheService.php | 1 + config/federation.php | 2 ++ 4 files changed, 6 insertions(+) diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php index 98e16bbc0..f1c2ca3ab 100644 --- a/app/Http/Controllers/Admin/AdminSettingsController.php +++ b/app/Http/Controllers/Admin/AdminSettingsController.php @@ -531,6 +531,7 @@ trait AdminSettingsController 'registration_status' => 'required|in:open,filtered,closed', 'cloud_storage' => 'required', 'activitypub_enabled' => 'required', + 'authorized_fetch' => 'required', 'account_migration' => 'required', 'mobile_apis' => 'required', 'stories' => 'required', @@ -555,6 +556,7 @@ trait AdminSettingsController } } } + ConfigCacheService::put('federation.activitypub.authorized_fetch', $request->boolean('authorized_fetch')); ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled')); ConfigCacheService::put('federation.migration', $request->boolean('account_migration')); ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis')); diff --git a/app/Services/AdminSettingsService.php b/app/Services/AdminSettingsService.php index 57fb6e96f..6a261f5a3 100644 --- a/app/Services/AdminSettingsService.php +++ b/app/Services/AdminSettingsService.php @@ -37,6 +37,7 @@ class AdminSettingsService 'registration_status' => $regState, 'cloud_storage' => $cloud_ready && $cloud_storage, 'activitypub_enabled' => (bool) config_cache('federation.activitypub.enabled'), + 'authorized_fetch' => (bool) config_cache('federation.activitypub.authorized_fetch'), 'account_migration' => (bool) config_cache('federation.migration'), 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), 'stories' => (bool) config_cache('instance.stories.enabled'), diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php index 4f2b006cc..527c86026 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -46,6 +46,7 @@ class ConfigCacheService 'pixelfed.oauth_enabled', 'pixelfed.import.instagram.enabled', 'pixelfed.bouncer.enabled', + 'federation.activitypub.authorized_fetch', 'pixelfed.enforce_email_verification', 'pixelfed.max_account_size', diff --git a/config/federation.php b/config/federation.php index 3d7a7bb30..124935ec8 100644 --- a/config/federation.php +++ b/config/federation.php @@ -30,6 +30,8 @@ return [ 'ingest' => [ 'store_notes_without_followers' => env('AP_INGEST_STORE_NOTES_WITHOUT_FOLLOWERS', false), ], + + 'authorized_fetch' => env('AUTHORIZED_FETCH', false), ], 'atom' => [ From a7b1cc9988bcfc415e97d4fab204439be3a328e7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 1 Aug 2024 21:49:35 -0600 Subject: [PATCH 2/4] Update AdminSettings, add authorized_fetch setting --- .../assets/components/admin/AdminSettings.vue | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/resources/assets/components/admin/AdminSettings.vue b/resources/assets/components/admin/AdminSettings.vue index 78ffd1b14..8693f05bd 100644 --- a/resources/assets/components/admin/AdminSettings.vue +++ b/resources/assets/components/admin/AdminSettings.vue @@ -63,6 +63,13 @@ @change="handleChange($event, 'features', 'activitypub_enabled')" /> + + + + { this.isSubmitting = false; this.isSubmittingTimeout = true; From 9847fbd8983655f42f023c0dd0b3d257ca3bf64e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 2 Aug 2024 00:12:34 -0600 Subject: [PATCH 3/4] Update composer --- composer.json | 1 + composer.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b97ec1187..29fd22327 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "laravel/ui": "^4.2", "league/flysystem-aws-s3-v3": "^3.0", "league/iso3166": "^2.1|^4.0", + "league/uri": "^7.4", "pbmedia/laravel-ffmpeg": "^8.0", "phpseclib/phpseclib": "~2.0", "pixelfed/fractal": "^0.18.0", diff --git a/composer.lock b/composer.lock index e6fd309e8..370627f1b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c055c4b1ba26004ab6951e9dba4b4508", + "content-hash": "fecc0efcc40880a422690feefedef584", "packages": [ { "name": "aws/aws-crt-php", From 8fea82150433c80ac5b9867bac6e39ec1e9e3e45 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 2 Aug 2024 01:40:06 -0600 Subject: [PATCH 4/4] Update AP helpers --- app/Util/ActivityPub/Helpers.php | 88 ++++---- app/Util/ActivityPub/HttpSignature.php | 265 +++++++++++++------------ 2 files changed, 193 insertions(+), 160 deletions(-) diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 9e03beef1..fe82eb2e8 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -25,6 +25,8 @@ use Cache; use Carbon\Carbon; use Illuminate\Support\Str; use Illuminate\Validation\Rule; +use League\Uri\Exceptions\UriException; +use League\Uri\Uri; use Purify; use Validator; @@ -153,61 +155,74 @@ class Helpers return in_array($url, $audience['to']) || in_array($url, $audience['cc']); } - public static function validateUrl($url) + public static function validateUrl($url = null, $disableDNSCheck = false) { - if (is_array($url)) { + if (is_array($url) && ! empty($url)) { $url = $url[0]; } + if (! $url || strlen($url) === 0) { + return false; + } + try { + $uri = Uri::new($url); - $hash = hash('sha256', $url); - $key = "helpers:url:valid:sha256-{$hash}"; + if (! $uri) { + return false; + } + + if ($uri->getScheme() !== 'https') { + return false; + } + + $host = $uri->getHost(); + + if (! $host || $host === '') { + return false; + } + + if (! filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { + return false; + } + + if (! str_contains($host, '.')) { + return false; + } - $valid = Cache::remember($key, 900, function () use ($url) { $localhosts = [ - '127.0.0.1', 'localhost', '::1', + 'localhost', + '127.0.0.1', + '::1', + 'broadcasthost', + 'ip6-localhost', + 'ip6-loopback', ]; - if (strtolower(mb_substr($url, 0, 8)) !== 'https://') { - return false; - } - - if (substr_count($url, '://') !== 1) { - return false; - } - - if (mb_substr($url, 0, 8) !== 'https://') { - $url = 'https://'.substr($url, 8); - } - - $valid = filter_var($url, FILTER_VALIDATE_URL); - - if (! $valid) { - return false; - } - - $host = parse_url($valid, PHP_URL_HOST); - if (in_array($host, $localhosts)) { return false; } - if (config('security.url.verify_dns')) { - if (DomainService::hasValidDns($host) === false) { + if ($disableDNSCheck !== true && app()->environment() === 'production' && (bool) config('security.url.verify_dns')) { + $hash = hash('sha256', $host); + $key = "helpers:url:valid-dns:sha256-{$hash}"; + $domainValidDns = Cache::remember($key, 14440, function () use ($host) { + return DomainService::hasValidDns($host); + }); + if (! $domainValidDns) { return false; } } - if (app()->environment() === 'production') { + if ($disableDNSCheck !== true && app()->environment() === 'production') { $bannedInstances = InstanceService::getBannedDomains(); if (in_array($host, $bannedInstances)) { return false; } } - return $url; - }); - - return $valid; + return $uri->toString(); + } catch (UriException $e) { + return false; + } } public static function validateLocalUrl($url) @@ -215,7 +230,12 @@ class Helpers $url = self::validateUrl($url); if ($url == true) { $domain = config('pixelfed.domain.app'); - $host = parse_url($url, PHP_URL_HOST); + + $uri = Uri::new($url); + $host = $uri->getHost(); + if (! $host || empty($host)) { + return false; + } $url = strtolower($domain) === strtolower($host) ? $url : false; return $url; diff --git a/app/Util/ActivityPub/HttpSignature.php b/app/Util/ActivityPub/HttpSignature.php index 5bfdcac09..35facb82b 100644 --- a/app/Util/ActivityPub/HttpSignature.php +++ b/app/Util/ActivityPub/HttpSignature.php @@ -2,146 +2,159 @@ namespace App\Util\ActivityPub; -use Cache, Log; use App\Models\InstanceActor; use App\Profile; -use \DateTime; +use Cache; +use DateTime; -class HttpSignature { +class HttpSignature +{ + /* + * source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php + * thanks aaronpk! + */ - /* - * source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php - * thanks aaronpk! - */ + public static function sign(Profile $profile, $url, $body = false, $addlHeaders = []) + { + if ($body) { + $digest = self::_digest($body); + } + $user = $profile; + $headers = self::_headersToSign($url, $body ? $digest : false); + $headers = array_merge($headers, $addlHeaders); + $stringToSign = self::_headersToSigningString($headers); + $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); + $key = openssl_pkey_get_private($user->private_key); + openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); + $signature = base64_encode($signature); + $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; + unset($headers['(request-target)']); + $headers['Signature'] = $signatureHeader; - public static function sign(Profile $profile, $url, $body = false, $addlHeaders = []) { - if($body) { - $digest = self::_digest($body); - } - $user = $profile; - $headers = self::_headersToSign($url, $body ? $digest : false); - $headers = array_merge($headers, $addlHeaders); - $stringToSign = self::_headersToSigningString($headers); - $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); - $key = openssl_pkey_get_private($user->private_key); - openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); - $signature = base64_encode($signature); - $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; - unset($headers['(request-target)']); - $headers['Signature'] = $signatureHeader; - - return self::_headersToCurlArray($headers); - } - - public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post') - { - $keyId = config('app.url') . '/i/actor#main-key'; - $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function() { - return InstanceActor::first()->private_key; - }); - if($body) { - $digest = self::_digest($body); - } - $headers = self::_headersToSign($url, $body ? $digest : false, $method); - $headers = array_merge($headers, $addlHeaders); - $stringToSign = self::_headersToSigningString($headers); - $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); - $key = openssl_pkey_get_private($privateKey); - openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); - $signature = base64_encode($signature); - $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; - unset($headers['(request-target)']); - $headers['Signature'] = $signatureHeader; - - return $headers; - } - - public static function parseSignatureHeader($signature) { - $parts = explode(',', $signature); - $signatureData = []; - - foreach($parts as $part) { - if(preg_match('/(.+)="(.+)"/', $part, $match)) { - $signatureData[$match[1]] = $match[2]; - } + return self::_headersToCurlArray($headers); } - if(!isset($signatureData['keyId'])) { - return [ - 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData)) - ]; + public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post') + { + $keyId = config('app.url').'/i/actor#main-key'; + $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () { + return InstanceActor::first()->private_key; + }); + if ($body) { + $digest = self::_digest($body); + } + $headers = self::_headersToSign($url, $body ? $digest : false, $method); + $headers = array_merge($headers, $addlHeaders); + $stringToSign = self::_headersToSigningString($headers); + $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); + $key = openssl_pkey_get_private($privateKey); + openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256); + $signature = base64_encode($signature); + $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"'; + unset($headers['(request-target)']); + $headers['Signature'] = $signatureHeader; + + return $headers; } - if(!filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) { - return [ - 'error' => 'keyId is not a URL: '.$signatureData['keyId'] - ]; + public static function parseSignatureHeader($signature) + { + $parts = explode(',', $signature); + $signatureData = []; + + foreach ($parts as $part) { + if (preg_match('/(.+)="(.+)"/', $part, $match)) { + $signatureData[$match[1]] = $match[2]; + } + } + + if (! isset($signatureData['keyId'])) { + return [ + 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData)), + ]; + } + + if (! filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) { + return [ + 'error' => 'keyId is not a URL: '.$signatureData['keyId'], + ]; + } + + if (! Helpers::validateUrl($signatureData['keyId'])) { + return [ + 'error' => 'keyId is not a URL: '.$signatureData['keyId'], + ]; + } + + if (! isset($signatureData['headers']) || ! isset($signatureData['signature'])) { + return [ + 'error' => 'Signature is missing headers or signature parts', + ]; + } + + return $signatureData; } - if(!isset($signatureData['headers']) || !isset($signatureData['signature'])) { - return [ - 'error' => 'Signature is missing headers or signature parts' - ]; + public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body) + { + $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true)); + $headersToSign = []; + foreach (explode(' ', $signatureData['headers']) as $h) { + if ($h == '(request-target)') { + $headersToSign[$h] = 'post '.$path; + } elseif ($h == 'digest') { + $headersToSign[$h] = $digest; + } elseif (isset($inputHeaders[$h][0])) { + $headersToSign[$h] = $inputHeaders[$h][0]; + } + } + $signingString = self::_headersToSigningString($headersToSign); + + $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256); + + return [$verified, $signingString]; } - return $signatureData; - } - - public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body) { - $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true)); - $headersToSign = []; - foreach(explode(' ',$signatureData['headers']) as $h) { - if($h == '(request-target)') { - $headersToSign[$h] = 'post '.$path; - } elseif($h == 'digest') { - $headersToSign[$h] = $digest; - } elseif(isset($inputHeaders[$h][0])) { - $headersToSign[$h] = $inputHeaders[$h][0]; - } - } - $signingString = self::_headersToSigningString($headersToSign); - - $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256); - - return [$verified, $signingString]; - } - - private static function _headersToSigningString($headers) { - return implode("\n", array_map(function($k, $v){ - return strtolower($k).': '.$v; - }, array_keys($headers), $headers)); - } - - private static function _headersToCurlArray($headers) { - return array_map(function($k, $v){ - return "$k: $v"; - }, array_keys($headers), $headers); - } - - private static function _digest($body) { - if(is_array($body)) { - $body = json_encode($body); - } - return base64_encode(hash('sha256', $body, true)); - } - - protected static function _headersToSign($url, $digest = false, $method = 'post') { - $date = new DateTime('UTC'); - - if(!in_array($method, ['post', 'get'])) { - throw new \Exception('Invalid method used to sign headers in HttpSignature'); - } - $headers = [ - '(request-target)' => $method . ' '.parse_url($url, PHP_URL_PATH), - 'Host' => parse_url($url, PHP_URL_HOST), - 'Date' => $date->format('D, d M Y H:i:s \G\M\T'), - ]; - - if($digest) { - $headers['Digest'] = 'SHA-256='.$digest; + private static function _headersToSigningString($headers) + { + return implode("\n", array_map(function ($k, $v) { + return strtolower($k).': '.$v; + }, array_keys($headers), $headers)); } - return $headers; - } + private static function _headersToCurlArray($headers) + { + return array_map(function ($k, $v) { + return "$k: $v"; + }, array_keys($headers), $headers); + } + private static function _digest($body) + { + if (is_array($body)) { + $body = json_encode($body); + } + + return base64_encode(hash('sha256', $body, true)); + } + + protected static function _headersToSign($url, $digest = false, $method = 'post') + { + $date = new DateTime('UTC'); + + if (! in_array($method, ['post', 'get'])) { + throw new \Exception('Invalid method used to sign headers in HttpSignature'); + } + $headers = [ + '(request-target)' => $method.' '.parse_url($url, PHP_URL_PATH), + 'Host' => parse_url($url, PHP_URL_HOST), + 'Date' => $date->format('D, d M Y H:i:s \G\M\T'), + ]; + + if ($digest) { + $headers['Digest'] = 'SHA-256='.$digest; + } + + return $headers; + } }