HttpSignature.php 7.1 KB

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