HttpSignature.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
  67. return InstanceActor::first()->private_key;
  68. });
  69. if ($body) {
  70. $digest = self::_digest($body);
  71. }
  72. $headers = self::_headersToSign($url, $body ? $digest : false, $method);
  73. $headers = array_merge($headers, $addlHeaders);
  74. $stringToSign = self::_headersToSigningString($headers);
  75. $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
  76. $key = openssl_pkey_get_private($privateKey);
  77. openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
  78. $signature = base64_encode($signature);
  79. $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
  80. unset($headers['(request-target)']);
  81. $headers['Signature'] = $signatureHeader;
  82. return $headers;
  83. }
  84. public static function parseSignatureHeader($signature)
  85. {
  86. $parts = explode(',', $signature);
  87. $signatureData = [];
  88. foreach ($parts as $part) {
  89. if (preg_match('/(.+)="(.+)"/', $part, $match)) {
  90. $signatureData[$match[1]] = $match[2];
  91. }
  92. }
  93. if (! isset($signatureData['keyId'])) {
  94. return [
  95. 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData)),
  96. ];
  97. }
  98. if (! filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
  99. return [
  100. 'error' => 'keyId is not a URL: '.$signatureData['keyId'],
  101. ];
  102. }
  103. if (! Helpers::validateUrl($signatureData['keyId'])) {
  104. return [
  105. 'error' => 'keyId is not a URL: '.$signatureData['keyId'],
  106. ];
  107. }
  108. if (! isset($signatureData['headers']) || ! isset($signatureData['signature'])) {
  109. return [
  110. 'error' => 'Signature is missing headers or signature parts',
  111. ];
  112. }
  113. return $signatureData;
  114. }
  115. public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body)
  116. {
  117. $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true));
  118. $headersToSign = [];
  119. foreach (explode(' ', $signatureData['headers']) as $h) {
  120. if ($h == '(request-target)') {
  121. $headersToSign[$h] = 'post '.$path;
  122. } elseif ($h == 'digest') {
  123. $headersToSign[$h] = $digest;
  124. } elseif (isset($inputHeaders[$h][0])) {
  125. $headersToSign[$h] = $inputHeaders[$h][0];
  126. }
  127. }
  128. $signingString = self::_headersToSigningString($headersToSign);
  129. $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
  130. return [$verified, $signingString];
  131. }
  132. private static function _headersToSigningString($headers)
  133. {
  134. return implode("\n", array_map(function ($k, $v) {
  135. return strtolower($k).': '.$v;
  136. }, array_keys($headers), $headers));
  137. }
  138. private static function _headersToCurlArray($headers)
  139. {
  140. return array_map(function ($k, $v) {
  141. return "$k: $v";
  142. }, array_keys($headers), $headers);
  143. }
  144. private static function _digest($body)
  145. {
  146. if (is_array($body)) {
  147. $body = json_encode($body);
  148. }
  149. return base64_encode(hash('sha256', $body, true));
  150. }
  151. protected static function _headersToSign($url, $digest = false, $method = 'post')
  152. {
  153. $date = new DateTime('UTC');
  154. if (! in_array($method, ['post', 'get'])) {
  155. throw new \Exception('Invalid method used to sign headers in HttpSignature');
  156. }
  157. $headers = [
  158. '(request-target)' => $method.' '.parse_url($url, PHP_URL_PATH),
  159. 'Host' => parse_url($url, PHP_URL_HOST),
  160. 'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
  161. ];
  162. if ($digest) {
  163. $headers['Digest'] = 'SHA-256='.$digest;
  164. }
  165. return $headers;
  166. }
  167. }