]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/lib/magicenvelope.php
Merge branch '0.9.x' into testing
[quix0rs-gnu-social.git] / plugins / OStatus / lib / magicenvelope.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
6  * A sample module to show best practices for StatusNet plugins
7  *
8  * PHP version 5
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU Affero General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Affero General Public License for more details.
19  *
20  * You should have received a copy of the GNU Affero General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  *
23  * @package   StatusNet
24  * @author    James Walker <james@status.net>
25  * @copyright 2010 StatusNet, Inc.
26  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
27  * @link      http://status.net/
28  */
29
30 class MagicEnvelope
31 {
32     const ENCODING = 'base64url';
33
34     const NS = 'http://salmon-protocol.org/ns/magic-env';
35
36     private function normalizeUser($user_id)
37     {
38         if (substr($user_id, 0, 5) == 'http:' ||
39             substr($user_id, 0, 6) == 'https:' ||
40             substr($user_id, 0, 5) == 'acct:') {
41             return $user_id;
42         }
43
44         if (strpos($user_id, '@') !== FALSE) {
45             return 'acct:' . $user_id;
46         }
47
48         return 'http://' . $user_id;
49     }
50
51     public function getKeyPair($signer_uri)
52     {
53         $disco = new Discovery();
54
55         try {
56             $xrd = $disco->lookup($signer_uri);
57         } catch (Exception $e) {
58             return false;
59         }
60         if ($xrd->links) {
61             if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) {
62                 $keypair = false;
63                 $parts = explode(',', $link['href']);
64                 if (count($parts) == 2) {
65                     $keypair = $parts[1];
66                 } else {
67                     // Backwards compatibility check for separator bug in 0.9.0
68                     $parts = explode(';', $link['href']);
69                     if (count($parts) == 2) {
70                         $keypair = $parts[1];
71                     }
72                 }
73
74                 if ($keypair) {
75                     return $keypair;
76                 }
77             }
78         }
79         // TRANS: Exception.
80         throw new Exception(_m('Unable to locate signer public key.'));
81     }
82
83     /**
84      * The current MagicEnvelope spec as used in StatusNet 0.9.7 and later
85      * includes both the original data and some signing metadata fields as
86      * the input plaintext for the signature hash.
87      *
88      * @param array $env
89      * @return string
90      */
91     public function signingText($env) {
92         return implode('.', array($env['data'], // this field is pre-base64'd
93                             Magicsig::base64_url_encode($env['data_type']),
94                             Magicsig::base64_url_encode($env['encoding']),
95                             Magicsig::base64_url_encode($env['alg'])));
96     }
97
98     /**
99      *
100      * @param <type> $text
101      * @param <type> $mimetype
102      * @param <type> $keypair
103      * @return array: associative array of envelope properties
104      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
105      */
106     public function signMessage($text, $mimetype, $keypair)
107     {
108         $signature_alg = Magicsig::fromString($keypair);
109         $armored_text = Magicsig::base64_url_encode($text);
110         $env = array(
111             'data' => $armored_text,
112             'encoding' => MagicEnvelope::ENCODING,
113             'data_type' => $mimetype,
114             'sig' => '',
115             'alg' => $signature_alg->getName()
116         );
117
118         $env['sig'] = $signature_alg->sign($this->signingText($env));
119
120         return $env;
121     }
122
123     /**
124      * Create an <me:env> XML representation of the envelope.
125      *
126      * @param array $env associative array with envelope data
127      * @return string representation of XML document
128      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
129      */
130     public function toXML($env) {
131         $xs = new XMLStringer();
132         $xs->startXML();
133         $xs->elementStart('me:env', array('xmlns:me' => MagicEnvelope::NS));
134         $xs->element('me:data', array('type' => $env['data_type']), $env['data']);
135         $xs->element('me:encoding', null, $env['encoding']);
136         $xs->element('me:alg', null, $env['alg']);
137         $xs->element('me:sig', null, $env['sig']);
138         $xs->elementEnd('me:env');
139
140         $string =  $xs->getString();
141         common_debug($string);
142         return $string;
143     }
144
145     /**
146      * Extract the contained XML payload, and insert a copy of the envelope
147      * signature data as an <me:provenance> section.
148      *
149      * @param array $env associative array with envelope data
150      * @return string representation of modified XML document
151      *
152      * @fixme in case of XML parsing errors, this will spew to the error log or output
153      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
154      */
155     public function unfold($env)
156     {
157         $dom = new DOMDocument();
158         $dom->loadXML(Magicsig::base64_url_decode($env['data']));
159
160         if ($dom->documentElement->tagName != 'entry') {
161             return false;
162         }
163
164         $prov = $dom->createElementNS(MagicEnvelope::NS, 'me:provenance');
165         $prov->setAttribute('xmlns:me', MagicEnvelope::NS);
166         $data = $dom->createElementNS(MagicEnvelope::NS, 'me:data', $env['data']);
167         $data->setAttribute('type', $env['data_type']);
168         $prov->appendChild($data);
169         $enc = $dom->createElementNS(MagicEnvelope::NS, 'me:encoding', $env['encoding']);
170         $prov->appendChild($enc);
171         $alg = $dom->createElementNS(MagicEnvelope::NS, 'me:alg', $env['alg']);
172         $prov->appendChild($alg);
173         $sig = $dom->createElementNS(MagicEnvelope::NS, 'me:sig', $env['sig']);
174         $prov->appendChild($sig);
175
176         $dom->documentElement->appendChild($prov);
177
178         return $dom->saveXML();
179     }
180
181     /**
182      * Find the author URI referenced in the given Atom entry.
183      *
184      * @param string $text string containing Atom entry XML
185      * @return mixed URI string or false if XML parsing fails, or null if no author URI can be found
186      *
187      * @fixme XML parsing failures will spew to error logs/output
188      */
189     public function getAuthor($text) {
190         $doc = new DOMDocument();
191         if (!$doc->loadXML($text)) {
192             return FALSE;
193         }
194
195         if ($doc->documentElement->tagName == 'entry') {
196             $authors = $doc->documentElement->getElementsByTagName('author');
197             foreach ($authors as $author) {
198                 $uris = $author->getElementsByTagName('uri');
199                 foreach ($uris as $uri) {
200                     return $this->normalizeUser($uri->nodeValue);
201                 }
202             }
203         }
204     }
205
206     /**
207      * Check if the author in the Atom entry fragment claims to match
208      * the given identifier URI.
209      *
210      * @param string $text string containing Atom entry XML
211      * @param string $signer_uri
212      * @return boolean
213      */
214     public function checkAuthor($text, $signer_uri)
215     {
216         return ($this->getAuthor($text) == $signer_uri);
217     }
218
219     /**
220      * Attempt to verify cryptographic signing for parsed envelope data.
221      * Requires network access to retrieve public key referenced by the envelope signer.
222      *
223      * Details of failure conditions are dumped to output log and not exposed to caller.
224      *
225      * @param array $env array representation of magic envelope data, as returned from MagicEnvelope::parse()
226      * @return boolean
227      *
228      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
229      */
230     public function verify($env)
231     {
232         if ($env['alg'] != 'RSA-SHA256') {
233             common_log(LOG_DEBUG, "Salmon error: bad algorithm");
234             return false;
235         }
236
237         if ($env['encoding'] != MagicEnvelope::ENCODING) {
238             common_log(LOG_DEBUG, "Salmon error: bad encoding");
239             return false;
240         }
241
242         $text = Magicsig::base64_url_decode($env['data']);
243         $signer_uri = $this->getAuthor($text);
244
245         try {
246             $keypair = $this->getKeyPair($signer_uri);
247         } catch (Exception $e) {
248             common_log(LOG_DEBUG, "Salmon error: ".$e->getMessage());
249             return false;
250         }
251
252         $verifier = Magicsig::fromString($keypair);
253
254         if (!$verifier) {
255             common_log(LOG_DEBUG, "Salmon error: unable to parse keypair");
256             return false;
257         }
258
259         return $verifier->verify($this->signingText($env), $env['sig']);
260     }
261
262     /**
263      * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
264      *
265      * @param string XML source
266      * @return mixed associative array of envelope data, or false on unrecognized input
267      *
268      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
269      * @fixme will spew errors to logs or output in case of XML parse errors
270      * @fixme may give fatal errors if some elements are missing or invalid XML
271      * @fixme calling DOMDocument::loadXML statically triggers warnings in strict mode
272      */
273     public function parse($text)
274     {
275         $dom = DOMDocument::loadXML($text);
276         return $this->fromDom($dom);
277     }
278
279     /**
280      * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element.
281      *
282      * @param DOMDocument $dom
283      * @return mixed associative array of envelope data, or false on unrecognized input
284      *
285      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around
286      * @fixme may give fatal errors if some elements are missing
287      */
288     public function fromDom($dom)
289     {
290         $env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'env')->item(0);
291         if (!$env_element) {
292             $env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'provenance')->item(0);
293         }
294
295         if (!$env_element) {
296             return false;
297         }
298
299         $data_element = $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'data')->item(0);
300         $sig_element = $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'sig')->item(0);
301         return array(
302             'data' => preg_replace('/\s/', '', $data_element->nodeValue),
303             'data_type' => $data_element->getAttribute('type'),
304             'encoding' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'encoding')->item(0)->nodeValue,
305             'alg' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'alg')->item(0)->nodeValue,
306             'sig' => preg_replace('/\s/', '', $sig_element->nodeValue),
307         );
308     }
309 }
310
311 /**
312  * Variant of MagicEnvelope using the earlier signature form listed in the MagicEnvelope
313  * spec in early 2010; this was used in StatusNet up through 0.9.6, so for backwards compatiblity
314  * we still need to accept and sometimes send this format.
315  */
316 class MagicEnvelopeCompat extends MagicEnvelope {
317
318     /**
319      * StatusNet through 0.9.6 used an earlier version of the MagicEnvelope spec
320      * which used only the input data, without the additional fields, as the plaintext
321      * for signing.
322      *
323      * @param array $env
324      * @return string
325      */
326     public function signingText($env) {
327         return $env['data'];
328     }
329 }
330