]> git.mxchange.org Git - friendica.git/blob - src/Util/HTTPSig.php
port hubzillas OpenWebAuth - remote authentification
[friendica.git] / src / Util / HTTPSig.php
1 <?php
2
3 /**
4  * @file src/Util/HTTPSig.php
5  */
6 namespace Friendica\Util;
7
8 use Friendica\Core\Config;
9 use Friendica\Util\Crypto;
10 use Friendica\Util\HTTPHeaders;
11
12 /**
13  * @brief Implements HTTP Signatures per draft-cavage-http-signatures-07.
14  *
15  * @see https://tools.ietf.org/html/draft-cavage-http-signatures-07
16  */
17
18 class HTTPSig
19 {
20         /**
21          * @brief RFC5843
22          *
23          * @see https://tools.ietf.org/html/rfc5843
24          *
25          * @param string $body The value to create the digest for
26          * @param boolean $set (optional, default true)
27          *   If set send a Digest HTTP header
28          * @return string The generated digest of $body
29          */
30         public static function generateDigest($body, $set = true)
31         {
32                 $digest = base64_encode(hash('sha256', $body, true));
33
34                 if($set) {
35                         header('Digest: SHA-256=' . $digest);
36                 }
37                 return $digest;
38         }
39
40         // See draft-cavage-http-signatures-08
41         public static function verify($data, $key = '')
42         {
43                 $body      = $data;
44                 $headers   = null;
45                 $spoofable = false;
46                 $result = [
47                         'signer'         => '',
48                         'header_signed'  => false,
49                         'header_valid'   => false,
50                         'content_signed' => false,
51                         'content_valid'  => false
52                 ];
53
54                 // Decide if $data arrived via controller submission or curl.
55                 if (is_array($data) && $data['header']) {
56                         if (!$data['success']) {
57                                 return $result;
58                         }
59
60                         $h = new HTTPHeaders($data['header']);
61                         $headers = $h->fetcharr();
62                         $body = $data['body'];
63                 } else {
64                         $headers = [];
65                         $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI'];
66
67                         foreach ($_SERVER as $k => $v) {
68                                 if (strpos($k, 'HTTP_') === 0) {
69                                         $field = str_replace('_', '-', strtolower(substr($k, 5)));
70                                         $headers[$field] = $v;
71                                 }
72                         }
73                 }
74
75                 $sig_block = null;
76
77                 if (array_key_exists('signature', $headers)) {
78                         $sig_block = self::parseSigheader($headers['signature']);
79                 } elseif (array_key_exists('authorization', $headers)) {
80                         $sig_block = self::parseSigheader($headers['authorization']);
81                 }
82
83                 if (!$sig_block) {
84                         logger('no signature provided.');
85                         return $result;
86                 }
87
88                 // Warning: This log statement includes binary data
89                 // logger('sig_block: ' . print_r($sig_block,true), LOGGER_DATA);
90
91                 $result['header_signed'] = true;
92
93                 $signed_headers = $sig_block['headers'];
94                 if (!$signed_headers) {
95                         $signed_headers = ['date'];
96                 }
97
98                 $signed_data = '';
99                 foreach ($signed_headers as $h) {
100                         if (array_key_exists($h, $headers)) {
101                                 $signed_data .= $h . ': ' . $headers[$h] . "\n";
102                         }
103                         if (strpos($h, '.')) {
104                                 $spoofable = true;
105                         }
106                 }
107
108                 $signed_data = rtrim($signed_data, "\n");
109
110                 $algorithm = null;
111                 if ($sig_block['algorithm'] === 'rsa-sha256') {
112                         $algorithm = 'sha256';
113                 }
114                 if ($sig_block['algorithm'] === 'rsa-sha512') {
115                         $algorithm = 'sha512';
116                 }
117
118                 if ($key && function_exists($key)) { /// @todo What function do we check for - maybe we check now for a method !!!
119                         $result['signer'] = $sig_block['keyId'];
120                         $key = $key($sig_block['keyId']);
121                 }
122
123                 if (!$key) {
124                         return $result;
125                 }
126
127                 $x = Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm);
128
129                 logger('verified: ' . $x, LOGGER_DEBUG);
130
131                 if (!$x) {
132                         return $result;
133                 }
134
135                 if (!$spoofable) {
136                         $result['header_valid'] = true;
137                 }
138
139                 if (in_array('digest', $signed_headers)) {
140                         $result['content_signed'] = true;
141                         $digest = explode('=', $headers['digest']);
142
143                         if ($digest[0] === 'SHA-256') {
144                                 $hashalg = 'sha256';
145                         }
146                         if ($digest[0] === 'SHA-512') {
147                                 $hashalg = 'sha512';
148                         }
149
150                         // The explode operation will have stripped the '=' padding, so compare against unpadded base64.
151                         if (rtrim(base64_encode(hash($hashalg, $body, true)), '=') === $digest[1]) {
152                                 $result['content_valid'] = true;
153                         }
154                 }
155
156                 logger('Content_Valid: ' . $result['content_valid']);
157
158                 return $result;
159         }
160
161         /**
162          * @brief
163          *
164          * @param string  $request
165          * @param array   $head
166          * @param string  $prvkey
167          * @param string  $keyid (optional, default 'Key')
168          * @param boolean $send_headers (optional, default false)
169          *   If set send a HTTP header
170          * @param boolean $auth (optional, default false)
171          * @param string  $alg (optional, default 'sha256')
172          * @param string  $crypt_key (optional, default null)
173          * @param string  $crypt_algo (optional, default 'aes256ctr')
174          * 
175          * @return array
176          */
177         public static function createSig($request, $head, $prvkey, $keyid = 'Key', $send_headers = false, $auth = false, $alg = 'sha256', $crypt_key = null, $crypt_algo = 'aes256ctr')
178         {
179                 $return_headers = [];
180
181                 if ($alg === 'sha256') {
182                         $algorithm = 'rsa-sha256';
183                 }
184
185                 if ($alg === 'sha512') {
186                         $algorithm = 'rsa-sha512';
187                 }
188
189                 $x = self::sign($request, $head, $prvkey, $alg);
190
191                 $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm
192                         . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"';
193
194                 if ($crypt_key) {
195                         $x = Crypto::encapsulate($headerval, $crypt_key, $crypt_algo);
196                         $headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"';
197                 }
198
199                 if ($auth) {
200                         $sighead = 'Authorization: Signature ' . $headerval;
201                 } else {
202                         $sighead = 'Signature: ' . $headerval;
203                 }
204
205                 if ($head) {
206                         foreach ($head as $k => $v) {
207                                 if ($send_headers) {
208                                         header($k . ': ' . $v);
209                                 } else {
210                                         $return_headers[] = $k . ': ' . $v;
211                                 }
212                         }
213                 }
214
215                 if ($send_headers) {
216                         header($sighead);
217                 } else {
218                         $return_headers[] = $sighead;
219                 }
220
221                 return $return_headers;
222         }
223
224         /**
225          * @brief
226          *
227          * @param string $request
228          * @param array  $head
229          * @param string $prvkey
230          * @param string $alg (optional) default 'sha256'
231          * 
232          * @return array
233          */
234         private static function sign($request, $head, $prvkey, $alg = 'sha256')
235         {
236                 $ret = [];
237                 $headers = '';
238                 $fields  = '';
239
240                 if ($request) {
241                         $headers = '(request-target)' . ': ' . trim($request) . "\n";
242                         $fields = '(request-target)';
243                 }
244
245                 if ($head) {
246                         foreach ($head as $k => $v) {
247                                 $headers .= strtolower($k) . ': ' . trim($v) . "\n";
248                                 if ($fields) {
249                                         $fields .= ' ';
250                                 }
251                                 $fields .= strtolower($k);
252                         }
253                         // strip the trailing linefeed
254                         $headers = rtrim($headers, "\n");
255                 }
256
257                 $sig = base64_encode(Crypto::rsaSign($headers, $prvkey, $alg));
258
259                 $ret['headers']   = $fields;
260                 $ret['signature'] = $sig;
261         
262                 return $ret;
263         }
264
265         /**
266          * @brief
267          *
268          * @param string $header
269          * @return array associate array with
270          *   - \e string \b keyID
271          *   - \e string \b algorithm
272          *   - \e array  \b headers
273          *   - \e string \b signature
274          */
275         public static function parseSigheader($header)
276         {
277                 $ret = [];
278                 $matches = [];
279
280                 // if the header is encrypted, decrypt with (default) site private key and continue
281                 if (preg_match('/iv="(.*?)"/ism', $header, $matches)) {
282                         $header = self::decryptSigheader($header);
283                 }
284
285                 if (preg_match('/keyId="(.*?)"/ism', $header, $matches)) {
286                         $ret['keyId'] = $matches[1];
287                 }
288
289                 if (preg_match('/algorithm="(.*?)"/ism', $header, $matches)) {
290                         $ret['algorithm'] = $matches[1];
291                 }
292
293                 if (preg_match('/headers="(.*?)"/ism', $header, $matches)) {
294                         $ret['headers'] = explode(' ', $matches[1]);
295                 }
296
297                 if (preg_match('/signature="(.*?)"/ism', $header, $matches)) {
298                         $ret['signature'] = base64_decode(preg_replace('/\s+/', '', $matches[1]));
299                 }
300
301                 if (($ret['signature']) && ($ret['algorithm']) && (!$ret['headers'])) {
302                         $ret['headers'] = ['date'];
303                 }
304
305                 return $ret;
306         }
307
308         /**
309          * @brief
310          *
311          * @param string $header
312          * @param string $prvkey (optional), if not set use site private key
313          * 
314          * @return array|string associative array, empty string if failue
315          *   - \e string \b iv
316          *   - \e string \b key
317          *   - \e string \b alg
318          *   - \e string \b data
319          */
320         private static function decryptSigheader($header, $prvkey = null)
321         {
322                 $iv = $key = $alg = $data = null;
323
324                 if (!$prvkey) {
325                         $prvkey = Config::get('system', 'prvkey');
326                 }
327
328                 $matches = [];
329
330                 if (preg_match('/iv="(.*?)"/ism', $header, $matches)) {
331                         $iv = $matches[1];
332                 }
333
334                 if (preg_match('/key="(.*?)"/ism', $header, $matches)) {
335                         $key = $matches[1];
336                 }
337
338                 if (preg_match('/alg="(.*?)"/ism', $header, $matches)) {
339                         $alg = $matches[1];
340                 }
341
342                 if (preg_match('/data="(.*?)"/ism', $header, $matches)) {
343                         $data = $matches[1];
344                 }
345
346                 if ($iv && $key && $alg && $data) {
347                         return Crypto::unencapsulate(['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data], $prvkey);
348                 }
349
350                 return '';
351         }
352 }