HttpSignature.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php
  2. namespace App\Util\ActivityPub;
  3. use App\Models\InstanceActor;
  4. use App\Profile;
  5. use Cache;
  6. use DateTime;
  7. class HttpSignature
  8. {
  9. /*
  10. * source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
  11. * thanks aaronpk!
  12. */
  13. public static function sign(Profile $profile, $url, $body = false, $addlHeaders = [])
  14. {
  15. if ($body) {
  16. $digest = self::_digest($body);
  17. }
  18. $user = $profile;
  19. $headers = self::_headersToSign($url, $body ? $digest : false);
  20. $headers = array_merge($headers, $addlHeaders);
  21. $stringToSign = self::_headersToSigningString($headers);
  22. $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
  23. $key = openssl_pkey_get_private($user->private_key);
  24. if (empty($key)) {
  25. return [];
  26. }
  27. openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
  28. if (empty($signature)) {
  29. return [];
  30. }
  31. $signature = base64_encode($signature);
  32. $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
  33. unset($headers['(request-target)']);
  34. $headers['Signature'] = $signatureHeader;
  35. return self::_headersToCurlArray($headers);
  36. }
  37. public static function signRaw($privateKey, $keyId, $url, $body = false, $addlHeaders = [])
  38. {
  39. if (empty($privateKey) || empty($keyId)) {
  40. return [];
  41. }
  42. if ($body) {
  43. $digest = self::_digest($body);
  44. }
  45. $headers = self::_headersToSign($url, $body ? $digest : false);
  46. $headers = array_merge($headers, $addlHeaders);
  47. $stringToSign = self::_headersToSigningString($headers);
  48. $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
  49. $key = openssl_pkey_get_private($privateKey);
  50. if (empty($key)) {
  51. return [];
  52. }
  53. openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
  54. if (empty($signature)) {
  55. return [];
  56. }
  57. $signature = base64_encode($signature);
  58. $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
  59. unset($headers['(request-target)']);
  60. $headers['Signature'] = $signatureHeader;
  61. return self::_headersToCurlArray($headers);
  62. }
  63. public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
  64. {
  65. $keyId = config('app.url').'/i/actor#main-key';
  66. if(config_cache('database.default') === 'mysql') {
  67. $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
  68. return InstanceActor::first()->private_key;
  69. });
  70. } else {
  71. $privateKey = InstanceActor::first()?->private_key;
  72. }
  73. abort_if(!$privateKey || empty($privateKey), 400, 'Missing instance actor key, please run php artisan instance:actor');
  74. if ($body) {
  75. $digest = self::_digest($body);
  76. }
  77. $headers = self::_headersToSign($url, $body ? $digest : false, $method);
  78. $headers = array_merge($headers, $addlHeaders);
  79. $stringToSign = self::_headersToSigningString($headers);
  80. $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
  81. $key = openssl_pkey_get_private($privateKey);
  82. openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
  83. $signature = base64_encode($signature);
  84. $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
  85. unset($headers['(request-target)']);
  86. $headers['Signature'] = $signatureHeader;
  87. return $headers;
  88. }
  89. public static function parseSignatureHeader($signature)
  90. {
  91. $parts = explode(',', $signature);
  92. $signatureData = [];
  93. foreach ($parts as $part) {
  94. if (preg_match('/(.+)="(.+)"/', $part, $match)) {
  95. $signatureData[$match[1]] = $match[2];
  96. }
  97. }
  98. if (! isset($signatureData['keyId'])) {
  99. return [
  100. 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData)),
  101. ];
  102. }
  103. if (! filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
  104. return [
  105. 'error' => 'keyId is not a URL: '.$signatureData['keyId'],
  106. ];
  107. }
  108. if (! Helpers::validateUrl($signatureData['keyId'])) {
  109. return [
  110. 'error' => 'keyId is not a URL: '.$signatureData['keyId'],
  111. ];
  112. }
  113. if (! isset($signatureData['headers']) || ! isset($signatureData['signature'])) {
  114. return [
  115. 'error' => 'Signature is missing headers or signature parts',
  116. ];
  117. }
  118. return $signatureData;
  119. }
  120. public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body)
  121. {
  122. $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true));
  123. $headersToSign = [];
  124. foreach (explode(' ', $signatureData['headers']) as $h) {
  125. if ($h == '(request-target)') {
  126. $headersToSign[$h] = 'post '.$path;
  127. } elseif ($h == 'digest') {
  128. $headersToSign[$h] = $digest;
  129. } elseif (isset($inputHeaders[$h][0])) {
  130. $headersToSign[$h] = $inputHeaders[$h][0];
  131. }
  132. }
  133. $signingString = self::_headersToSigningString($headersToSign);
  134. $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
  135. return [$verified, $signingString];
  136. }
  137. private static function _headersToSigningString($headers)
  138. {
  139. return implode("\n", array_map(function ($k, $v) {
  140. return strtolower($k).': '.$v;
  141. }, array_keys($headers), $headers));
  142. }
  143. private static function _headersToCurlArray($headers)
  144. {
  145. return array_map(function ($k, $v) {
  146. return "$k: $v";
  147. }, array_keys($headers), $headers);
  148. }
  149. private static function _digest($body)
  150. {
  151. if (is_array($body)) {
  152. $body = json_encode($body);
  153. }
  154. return base64_encode(hash('sha256', $body, true));
  155. }
  156. protected static function _headersToSign($url, $digest = false, $method = 'post')
  157. {
  158. $date = new DateTime('UTC');
  159. if (! in_array($method, ['post', 'get'])) {
  160. throw new \Exception('Invalid method used to sign headers in HttpSignature');
  161. }
  162. $headers = [
  163. '(request-target)' => $method.' '.parse_url($url, PHP_URL_PATH),
  164. 'Host' => parse_url($url, PHP_URL_HOST),
  165. 'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
  166. ];
  167. if ($digest) {
  168. $headers['Digest'] = 'SHA-256='.$digest;
  169. }
  170. return $headers;
  171. }
  172. }