]> git.mxchange.org Git - friendica.git/blob - src/Util/HTTPSignature.php
f6a5fe1fe4f869c142a1683bc522eb43e031ae7d
[friendica.git] / src / Util / HTTPSignature.php
1 <?php
2
3 /**
4  * @file src/Util/HTTPSignature.php
5  */
6 namespace Friendica\Util;
7
8 use Friendica\BaseObject;
9 use Friendica\Core\Config;
10 use Friendica\Database\DBA;
11 use Friendica\Model\User;
12 use Friendica\Protocol\ActivityPub;
13
14 /**
15  * @brief Implements HTTP Signatures per draft-cavage-http-signatures-07.
16  *
17  * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/Zotlabs/Web/HTTPSig.php
18  *
19  * @see https://tools.ietf.org/html/draft-cavage-http-signatures-07
20  */
21
22 class HTTPSignature
23 {
24         // See draft-cavage-http-signatures-08
25         public static function verifyMagic($key)
26         {
27                 $headers   = null;
28                 $spoofable = false;
29                 $result = [
30                         'signer'         => '',
31                         'header_signed'  => false,
32                         'header_valid'   => false
33                 ];
34
35                 // Decide if $data arrived via controller submission or curl.
36                 $headers = [];
37                 $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI'];
38
39                 foreach ($_SERVER as $k => $v) {
40                         if (strpos($k, 'HTTP_') === 0) {
41                                 $field = str_replace('_', '-', strtolower(substr($k, 5)));
42                                 $headers[$field] = $v;
43                         }
44                 }
45
46                 $sig_block = null;
47
48                 $sig_block = self::parseSigheader($headers['authorization']);
49
50                 if (!$sig_block) {
51                         logger('no signature provided.');
52                         return $result;
53                 }
54
55                 $result['header_signed'] = true;
56
57                 $signed_headers = $sig_block['headers'];
58                 if (!$signed_headers) {
59                         $signed_headers = ['date'];
60                 }
61
62                 $signed_data = '';
63                 foreach ($signed_headers as $h) {
64                         if (array_key_exists($h, $headers)) {
65                                 $signed_data .= $h . ': ' . $headers[$h] . "\n";
66                         }
67                         if (strpos($h, '.')) {
68                                 $spoofable = true;
69                         }
70                 }
71
72                 $signed_data = rtrim($signed_data, "\n");
73
74                 $algorithm = 'sha512';
75
76                 if ($key && function_exists($key)) {
77                         $result['signer'] = $sig_block['keyId'];
78                         $key = $key($sig_block['keyId']);
79                 }
80
81                 logger('Got keyID ' . $sig_block['keyId']);
82
83                 if (!$key) {
84                         return $result;
85                 }
86
87                 $x = Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm);
88
89                 logger('verified: ' . $x, LOGGER_DEBUG);
90
91                 if (!$x) {
92                         return $result;
93                 }
94
95                 if (!$spoofable) {
96                         $result['header_valid'] = true;
97                 }
98
99                 return $result;
100         }
101
102         /**
103          * @brief
104          *
105          * @param array   $head
106          * @param string  $prvkey
107          * @param string  $keyid (optional, default 'Key')
108          *
109          * @return array
110          */
111         public static function createSig($head, $prvkey, $keyid = 'Key')
112         {
113                 $return_headers = [];
114
115                 $alg = 'sha512';
116                 $algorithm = 'rsa-sha512';
117
118                 $x = self::sign($head, $prvkey, $alg);
119
120                 $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm
121                         . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"';
122
123                 $sighead = 'Authorization: Signature ' . $headerval;
124
125                 if ($head) {
126                         foreach ($head as $k => $v) {
127                                 $return_headers[] = $k . ': ' . $v;
128                         }
129                 }
130
131                 $return_headers[] = $sighead;
132
133                 return $return_headers;
134         }
135
136         /**
137          * @brief
138          *
139          * @param array  $head
140          * @param string $prvkey
141          * @param string $alg (optional) default 'sha256'
142          *
143          * @return array
144          */
145         private static function sign($head, $prvkey, $alg = 'sha256')
146         {
147                 $ret = [];
148                 $headers = '';
149                 $fields  = '';
150
151                 foreach ($head as $k => $v) {
152                         $headers .= strtolower($k) . ': ' . trim($v) . "\n";
153                         if ($fields) {
154                                 $fields .= ' ';
155                         }
156                         $fields .= strtolower($k);
157                 }
158                 // strip the trailing linefeed
159                 $headers = rtrim($headers, "\n");
160
161                 $sig = base64_encode(Crypto::rsaSign($headers, $prvkey, $alg));
162
163                 $ret['headers']   = $fields;
164                 $ret['signature'] = $sig;
165
166                 return $ret;
167         }
168
169         /**
170          * @brief
171          *
172          * @param string $header
173          * @return array associate array with
174          *   - \e string \b keyID
175          *   - \e string \b algorithm
176          *   - \e array  \b headers
177          *   - \e string \b signature
178          */
179         public static function parseSigheader($header)
180         {
181                 $ret = [];
182                 $matches = [];
183
184                 // if the header is encrypted, decrypt with (default) site private key and continue
185                 if (preg_match('/iv="(.*?)"/ism', $header, $matches)) {
186                         $header = self::decryptSigheader($header);
187                 }
188
189                 if (preg_match('/keyId="(.*?)"/ism', $header, $matches)) {
190                         $ret['keyId'] = $matches[1];
191                 }
192
193                 if (preg_match('/algorithm="(.*?)"/ism', $header, $matches)) {
194                         $ret['algorithm'] = $matches[1];
195                 }
196
197                 if (preg_match('/headers="(.*?)"/ism', $header, $matches)) {
198                         $ret['headers'] = explode(' ', $matches[1]);
199                 }
200
201                 if (preg_match('/signature="(.*?)"/ism', $header, $matches)) {
202                         $ret['signature'] = base64_decode(preg_replace('/\s+/', '', $matches[1]));
203                 }
204
205                 if (($ret['signature']) && ($ret['algorithm']) && (!$ret['headers'])) {
206                         $ret['headers'] = ['date'];
207                 }
208
209                 return $ret;
210         }
211
212         /**
213          * @brief
214          *
215          * @param string $header
216          * @param string $prvkey (optional), if not set use site private key
217          *
218          * @return array|string associative array, empty string if failue
219          *   - \e string \b iv
220          *   - \e string \b key
221          *   - \e string \b alg
222          *   - \e string \b data
223          */
224         private static function decryptSigheader($header, $prvkey = null)
225         {
226                 $iv = $key = $alg = $data = null;
227
228                 if (!$prvkey) {
229                         $prvkey = Config::get('system', 'prvkey');
230                 }
231
232                 $matches = [];
233
234                 if (preg_match('/iv="(.*?)"/ism', $header, $matches)) {
235                         $iv = $matches[1];
236                 }
237
238                 if (preg_match('/key="(.*?)"/ism', $header, $matches)) {
239                         $key = $matches[1];
240                 }
241
242                 if (preg_match('/alg="(.*?)"/ism', $header, $matches)) {
243                         $alg = $matches[1];
244                 }
245
246                 if (preg_match('/data="(.*?)"/ism', $header, $matches)) {
247                         $data = $matches[1];
248                 }
249
250                 if ($iv && $key && $alg && $data) {
251                         return Crypto::unencapsulate(['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data], $prvkey);
252                 }
253
254                 return '';
255         }
256
257         /**
258          * Functions for ActivityPub
259          */
260
261         public static function transmit($data, $target, $uid)
262         {
263                 $owner = User::getOwnerDataById($uid);
264
265                 if (!$owner) {
266                         return;
267                 }
268
269                 $content = json_encode($data);
270
271                 // Header data that is about to be signed.
272                 $host = parse_url($target, PHP_URL_HOST);
273                 $path = parse_url($target, PHP_URL_PATH);
274                 $digest = 'SHA-256=' . base64_encode(hash('sha256', $content, true));
275                 $content_length = strlen($content);
276
277                 $headers = ['Content-Length: ' . $content_length, 'Digest: ' . $digest, 'Host: ' . $host];
278
279                 $signed_data = "(request-target): post " . $path . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host;
280
281                 $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256'));
282
283                 $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) content-length digest host",signature="' . $signature . '"';
284
285                 $headers[] = 'Content-Type: application/activity+json';
286
287                 Network::post($target, $content, $headers);
288                 $return_code = BaseObject::getApp()->get_curl_code();
289
290                 logger('Transmit to ' . $target . ' returned ' . $return_code);
291         }
292
293         public static function verifyAP($content, $http_headers)
294         {
295                 $object = json_decode($content, true);
296
297                 if (empty($object)) {
298                         return false;
299                 }
300
301                 $actor = JsonLD::fetchElement($object, 'actor', 'id');
302
303                 $headers = [];
304                 $headers['(request-target)'] = strtolower($http_headers['REQUEST_METHOD']) . ' ' . $http_headers['REQUEST_URI'];
305
306                 // First take every header
307                 foreach ($http_headers as $k => $v) {
308                         $field = str_replace('_', '-', strtolower($k));
309                         $headers[$field] = $v;
310                 }
311
312                 // Now add every http header
313                 foreach ($http_headers as $k => $v) {
314                         if (strpos($k, 'HTTP_') === 0) {
315                                 $field = str_replace('_', '-', strtolower(substr($k, 5)));
316                                 $headers[$field] = $v;
317                         }
318                 }
319
320                 $sig_block = self::parseSigHeader($http_headers['HTTP_SIGNATURE']);
321
322                 if (empty($sig_block) || empty($sig_block['headers']) || empty($sig_block['keyId'])) {
323                         return false;
324                 }
325
326                 $signed_data = '';
327                 foreach ($sig_block['headers'] as $h) {
328                         if (array_key_exists($h, $headers)) {
329                                 $signed_data .= $h . ': ' . $headers[$h] . "\n";
330                         }
331                 }
332                 $signed_data = rtrim($signed_data, "\n");
333
334                 if (empty($signed_data)) {
335                         return false;
336                 }
337
338                 $algorithm = null;
339
340                 if ($sig_block['algorithm'] === 'rsa-sha256') {
341                         $algorithm = 'sha256';
342                 }
343
344                 if ($sig_block['algorithm'] === 'rsa-sha512') {
345                         $algorithm = 'sha512';
346                 }
347
348                 if (empty($algorithm)) {
349                         return false;
350                 }
351
352                 $key = self::fetchKey($sig_block['keyId'], $actor);
353
354                 if (empty($key)) {
355                         return false;
356                 }
357
358                 if (!Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm)) {
359                         return false;
360                 }
361
362                 // Check the digest when it is part of the signed data
363                 if (in_array('digest', $sig_block['headers'])) {
364                         $digest = explode('=', $headers['digest'], 2);
365                         if ($digest[0] === 'SHA-256') {
366                                 $hashalg = 'sha256';
367                         }
368                         if ($digest[0] === 'SHA-512') {
369                                 $hashalg = 'sha512';
370                         }
371
372                         /// @todo add all hashes from the rfc
373
374                         if (!empty($hashalg) && base64_encode(hash($hashalg, $content, true)) != $digest[1]) {
375                                 return false;
376                         }
377                 }
378
379                 // Check the content-length when it is part of the signed data
380                 if (in_array('content-length', $sig_block['headers'])) {
381                         if (strlen($content) != $headers['content-length']) {
382                                 return false;
383                         }
384                 }
385
386                 return true;
387
388         }
389
390         private static function fetchKey($id, $actor)
391         {
392                 $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id);
393
394                 $profile = ActivityPub::fetchprofile($url);
395                 if (!empty($profile)) {
396                         return $profile['pubkey'];
397                 } elseif ($url != $actor) {
398                         $profile = ActivityPub::fetchprofile($actor);
399                         if (!empty($profile)) {
400                                 return $profile['pubkey'];
401                         }
402                 }
403
404                 return false;
405         }
406 }