From 8fea82150433c80ac5b9867bac6e39ec1e9e3e45 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 2 Aug 2024 01:40:06 -0600 Subject: [PATCH] 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 9e03beef..fe82eb2e 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 5bfdcac0..35facb82 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; + } }