HttpSignature.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. <?php
  2. namespace App\Util\ActivityPub;
  3. use Cache, Log;
  4. use App\Models\InstanceActor;
  5. use App\Profile;
  6. use \DateTime;
  7. class HttpSignature {
  8. /*
  9. * source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
  10. * thanks aaronpk!
  11. */
  12. public static function sign(Profile $profile, $url, $body = false, $addlHeaders = []) {
  13. if($body) {
  14. $digest = self::_digest($body);
  15. }
  16. $user = $profile;
  17. $headers = self::_headersToSign($url, $body ? $digest : false);
  18. $headers = array_merge($headers, $addlHeaders);
  19. $stringToSign = self::_headersToSigningString($headers);
  20. $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
  21. $key = openssl_pkey_get_private($user->private_key);
  22. openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
  23. $signature = base64_encode($signature);
  24. $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
  25. unset($headers['(request-target)']);
  26. $headers['Signature'] = $signatureHeader;
  27. return self::_headersToCurlArray($headers);
  28. }
  29. public static function instanceActorSign($url, $body = false, $addlHeaders = [])
  30. {
  31. $keyId = config('app.url') . '/i/actor#main-key';
  32. $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function() {
  33. return InstanceActor::first()->private_key;
  34. });
  35. if($body) {
  36. $digest = self::_digest($body);
  37. }
  38. $headers = self::_headersToSign($url, $body ? $digest : false);
  39. $headers = array_merge($headers, $addlHeaders);
  40. $stringToSign = self::_headersToSigningString($headers);
  41. $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
  42. $key = openssl_pkey_get_private($privateKey);
  43. openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
  44. $signature = base64_encode($signature);
  45. $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
  46. unset($headers['(request-target)']);
  47. $headers['Signature'] = $signatureHeader;
  48. return $headers;
  49. }
  50. public static function parseSignatureHeader($signature) {
  51. $parts = explode(',', $signature);
  52. $signatureData = [];
  53. foreach($parts as $part) {
  54. if(preg_match('/(.+)="(.+)"/', $part, $match)) {
  55. $signatureData[$match[1]] = $match[2];
  56. }
  57. }
  58. if(!isset($signatureData['keyId'])) {
  59. return [
  60. 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData))
  61. ];
  62. }
  63. if(!filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
  64. return [
  65. 'error' => 'keyId is not a URL: '.$signatureData['keyId']
  66. ];
  67. }
  68. if(!isset($signatureData['headers']) || !isset($signatureData['signature'])) {
  69. return [
  70. 'error' => 'Signature is missing headers or signature parts'
  71. ];
  72. }
  73. return $signatureData;
  74. }
  75. public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body) {
  76. $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true));
  77. $headersToSign = [];
  78. foreach(explode(' ',$signatureData['headers']) as $h) {
  79. if($h == '(request-target)') {
  80. $headersToSign[$h] = 'post '.$path;
  81. } elseif($h == 'digest') {
  82. $headersToSign[$h] = $digest;
  83. } elseif(isset($inputHeaders[$h][0])) {
  84. $headersToSign[$h] = $inputHeaders[$h][0];
  85. }
  86. }
  87. $signingString = self::_headersToSigningString($headersToSign);
  88. $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
  89. return [$verified, $signingString];
  90. }
  91. private static function _headersToSigningString($headers) {
  92. return implode("\n", array_map(function($k, $v){
  93. return strtolower($k).': '.$v;
  94. }, array_keys($headers), $headers));
  95. }
  96. private static function _headersToCurlArray($headers) {
  97. return array_map(function($k, $v){
  98. return "$k: $v";
  99. }, array_keys($headers), $headers);
  100. }
  101. private static function _digest($body) {
  102. if(is_array($body)) {
  103. $body = json_encode($body);
  104. }
  105. return base64_encode(hash('sha256', $body, true));
  106. }
  107. protected static function _headersToSign($url, $digest = false) {
  108. $date = new DateTime('UTC');
  109. $headers = [
  110. '(request-target)' => 'post '.parse_url($url, PHP_URL_PATH),
  111. 'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
  112. 'Host' => parse_url($url, PHP_URL_HOST),
  113. 'Accept' => 'application/activity+json, application/json',
  114. ];
  115. if($digest) {
  116. $headers['Digest'] = 'SHA-256='.$digest;
  117. }
  118. return $headers;
  119. }
  120. }