From 5edee3c4d1f84c07c1c54775072601188075a542 Mon Sep 17 00:00:00 2001 From: Mike Macgirvin Date: Thu, 21 Oct 2010 04:53:43 -0700 Subject: [PATCH] magic-envelope verification, status.net appears to do it wrong. Ultimately we need to do it right (or why bother having a spec?), and fallback to doing it wrong if we're talking to a broken system - which ironically seems to include most of the federated social web projects. --- boot.php | 73 ++++++++++++++----- database.sql | 1 + include/items.php | 2 +- include/salmon.php | 109 +++++++++++++++++++++++++++++ library/asn1.php | 2 +- mod/salmon.php | 170 +++++++++++++++++++++++++++++++++++++++++++-- update.php | 6 +- view/fake_feed.tpl | 18 +++++ 8 files changed, 354 insertions(+), 27 deletions(-) create mode 100644 view/fake_feed.tpl diff --git a/boot.php b/boot.php index 8fec10afaa..55c244e827 100644 --- a/boot.php +++ b/boot.php @@ -2,7 +2,7 @@ set_time_limit(0); -define ( 'BUILD_ID', 1010 ); +define ( 'BUILD_ID', 1011 ); define ( 'DFRN_PROTOCOL_VERSION', '2.0' ); define ( 'EOL', "
\r\n" ); @@ -116,6 +116,7 @@ class App { private $db; private $curl_code; + private $curl_headers; function __construct() { @@ -204,6 +205,15 @@ class App { return $this->curl_code; } + function set_curl_headers($headers) { + $this->curl_headers = $headers; + } + + function get_curl_headers() { + return $this->curl_headers; + } + + }} // retrieve the App structure @@ -339,13 +349,12 @@ function t($s) { // results. if(! function_exists('fetch_url')) { -function fetch_url($url,$binary = false) { +function fetch_url($url,$binary = false, &$redirects = 0) { $ch = curl_init($url); - if(! $ch) return false; + if(($redirects > 8) || (! $ch)) + return false; - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION,true); - curl_setopt($ch, CURLOPT_MAXREDIRS,8); + curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER,true); // by default we will allow self-signed certs @@ -366,26 +375,41 @@ function fetch_url($url,$binary = false) { curl_setopt($ch, CURLOPT_BINARYTRANSFER,1); $s = curl_exec($ch); - $info = curl_getinfo($ch); + + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $header = substr($s,0,strpos($s,"\r\n\r\n")); + if($http_code == 301 || $http_code == 302 || $http_code == 303) { + $matches = array(); + preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches); + $url = trim(array_pop($matches)); + $url_parsed = parse_url($url); + if (isset($url_parsed)) { + $redirects++; + return fetch_url($url,$binary,$redirects); + } + } $a = get_app(); - $a->set_curl_code($info['http_code']); + $a->set_curl_code($http_code); + $body = substr($s,strlen($header)+4); + $a->set_curl_headers($header); + curl_close($ch); - return($s); + return($body); }} // post request to $url. $params is an array of post variables. if(! function_exists('post_url')) { -function post_url($url,$params) { +function post_url($url,$params, &$redirects = 0) { $ch = curl_init($url); - if(! $ch) return false; + if(($redirects > 8) || (! $ch)) + return false; - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION,true); - curl_setopt($ch, CURLOPT_MAXREDIRS,8); + curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER,true); curl_setopt($ch, CURLOPT_POST,1); curl_setopt($ch, CURLOPT_POSTFIELDS,$params); + $check_cert = get_config('system','verifyssl'); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false)); $prx = get_config('system','proxy'); @@ -398,11 +422,26 @@ function post_url($url,$params) { } $s = curl_exec($ch); - $info = curl_getinfo($ch); + + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $header = substr($s,0,strpos($s,"\r\n\r\n")); + if($http_code == 301 || $http_code == 302 || $http_code == 303) { + $matches = array(); + preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches); + $url = trim(array_pop($matches)); + $url_parsed = parse_url($url); + if (isset($url_parsed)) { + $redirects++; + return post_url($url,$binary,$redirects); + } + } $a = get_app(); - $a->set_curl_code($info['http_code']); + $a->set_curl_code($http_code); + $body = substr($s,strlen($header)+4); + $a->set_curl_headers($header); + curl_close($ch); - return($s); + return($body); }} // random hash, 64 chars diff --git a/database.sql b/database.sql index 7526be41d1..a3d78d79a3 100644 --- a/database.sql +++ b/database.sql @@ -62,6 +62,7 @@ CREATE TABLE IF NOT EXISTS `contact` ( `issued-id` char(255) NOT NULL, `dfrn-id` char(255) NOT NULL, `url` char(255) NOT NULL, + `lrdd` char(255) NOT NULL, `pubkey` text NOT NULL, `prvkey` text NOT NULL, `request` text NOT NULL, diff --git a/include/items.php b/include/items.php index f04bf0bd87..eca050d4fe 100644 --- a/include/items.php +++ b/include/items.php @@ -139,7 +139,7 @@ function get_feed_for(&$a, $dfrn_id, $owner_id, $last_update, $direction = 0) { } $salmon = '' . "\n" ; - $salmon = ''; // remove this line when salmon handler is finished +// $salmon = ''; // remove this line when salmon handler is finished $atom .= replace_macros($feed_template, array( '$feed_id' => xmlify($a->get_baseurl() . '/profile/' . $owner_nick), diff --git a/include/salmon.php b/include/salmon.php index 7198f07c60..bd2d620a81 100644 --- a/include/salmon.php +++ b/include/salmon.php @@ -16,3 +16,112 @@ function salmon_key($pubkey) { return 'RSA' . '.' . $m . '.' . $e ; } + + +function base64url_encode($s) { + return strtr(base64_encode($s),'+/','-_'); +} + +function base64url_decode($s) { + return base64_decode(strtr($s,'-_','+/')); +} + +function get_salmon_key($uri,$keyhash) { + $ret = array(); + + $debugging = get_config('system','debugging'); + if($debugging) + file_put_contents('salmon.out', "\n" . 'Fetch key' . "\n", FILE_APPEND); + + if(strstr($uri,'@')) { + $arr = webfinger($uri); + if($debugging) + file_put_contents('salmon.out', "\n" . 'Fetch key from webfinger' . "\n", FILE_APPEND); + } + else { + $html = fetch_url($uri); + $a = get_app(); + $h = $a->get_curl_headers(); + if($debugging) + file_put_contents('salmon.out', "\n" . 'Fetch key via HTML header: ' . $h . "\n", FILE_APPEND); + + $l = explode("\n",$h); + if(count($l)) { + foreach($l as $line) { + + if($debugging) + file_put_contents('salmon.out', "\n" . $line . "\n", FILE_APPEND); + if((stristr($line,'link:')) && preg_match('/<([^>].*)>.*rel\=[\'\"]lrdd[\'\"]/',$line,$matches)) { + $link = $matches[1]; + if($debugging) + file_put_contents('salmon.out', "\n" . 'Fetch key via Link from header: ' . $link . "\n", FILE_APPEND); + break; + } + } + } + } + + if(! isset($link)) { + require_once('library/HTML5/Parser.php'); + $dom = HTML5_Parser::parse($html); + + if(! $dom) + return ''; + + $items = $dom->getElementsByTagName('link'); + + foreach($items as $item) { + $x = $item->getAttribute('rel'); + if($x == "lrdd") { + $link = $item->getAttribute('href'); + if($debugging) + file_put_contents('salmon.out', "\n" . 'Fetch key via HTML body' . $link . "\n", FILE_APPEND); + break; + } + } + } + + if(! isset($link)) + return ''; + + $arr = fetch_xrd_links($link); + + if($arr) { + foreach($arr as $a) { + if($a['@attributes']['rel'] === 'magic-public-key') { + $ret[] = $a['@attributes']['href']; + } + } + } + if(count($ret)) { + for($x = 0; $x < count($ret); $x ++) { + if(substr($ret[$x],0,5) === 'data:') { + if(strstr($ret[$x],',')) + $ret[$x] = substr($ret[$x],strpos($ret[$x],',')+1); + else + $ret[$x] = substr($ret[$x],5); + } + else + $ret[$x] = fetch_url($ret[$x]); + } + } + if($debugging) + file_put_contents('salmon.out', "\n" . 'Key located: ' . print_r($ret,true) . "\n", FILE_APPEND); + + if(count($ret) == 1) { + return $ret[0]; + } + else { + foreach($ret as $a) { + $hash = base64url_encode(hash('sha256',$a)); + if($hash == $keyhash) + return $a; + } + } + + return ''; +} + + + + \ No newline at end of file diff --git a/library/asn1.php b/library/asn1.php index 132b032480..713978e8c1 100644 --- a/library/asn1.php +++ b/library/asn1.php @@ -186,7 +186,7 @@ class ASN_BASE { case ASN_BOOLEAN: return new ASN_BOOLEAN((bool)$data); case ASN_INTEGER: - return new ASN_INTEGER(strtr(base64_encode($data),'+/=','-_,')); + return new ASN_INTEGER(strtr(base64_encode($data),'+/','-_')); // return new ASN_INTEGER(ord($data)); case ASN_BIT_STR: return new ASN_BIT_STR(self::parseASNString($data, $level+1, $maxLevels)); diff --git a/mod/salmon.php b/mod/salmon.php index d68c746584..7ae29aafe2 100644 --- a/mod/salmon.php +++ b/mod/salmon.php @@ -1,5 +1,15 @@ = 500) @@ -18,7 +28,7 @@ function salmon_post(&$a) { $debugging = get_config('system','debugging'); if($debugging) - file_put_contents('salmon.out',$xml,FILE_APPEND); + file_put_contents('salmon.out','New Salmon: ' . $xml . "\n",FILE_APPEND); $nick = (($a->argc > 1) ? notags(trim($a->argv[1])) : ''); $mentions = (($a->argc > 2 && $a->argv[2] === 'mention') ? true : false); @@ -31,22 +41,168 @@ function salmon_post(&$a) { $importer = $r[0]; - require_once('include/items.php'); + // parse the xml + + $dom = simplexml_load_string($xml,'SimpleXMLElement',0,NAMESPACE_SALMON_ME); + + + if($debugging) + file_put_contents('salmon.out', "\n" . print_r($dom,true) . "\n" , FILE_APPEND); + + // figure out where in the DOM tree our data is hiding + + if($dom->provenance->data) + $base = $dom->provenance; + elseif($dom->env->data) + $base = $dom->env; + elseif($dom->data) + $base = $dom; + + if(! $base) { + if($debugging) + file_put_contents('salmon.out', "\n" . 'Unable to find salmon data in XML' . "\n" , FILE_APPEND); + salmon_return(500); + } + + // Stash the signature away for now. We have to find their key or it won't be good for anything. + + + $signature = base64url_decode($base->sig); + if($debugging) + file_put_contents('salmon.out', "\n" . 'Encoded Signature: ' . $base->sig . "\n" , FILE_APPEND); + + // unpack our data element. + + // strip whitespace + $data = str_replace(array(" ","\t","\r","\n"),array("","","",""),$base->data); + $type = $base->data[0]->attributes()->type[0]; + $encoding = $base->encoding; + $alg = $base->alg; + + $signed_data = $data; + // . '.' . base64url_encode($type) . '.' . base64url_encode($encoding) . '.' . base64url_encode($alg); + // decode it + $data = base64url_decode($data); + + if($debugging) + file_put_contents('salmon.out', "\n" . 'Signed data:>>>' . $signed_data . "<<<\n" , FILE_APPEND); + + // Remove the xml declaration + $data = preg_replace('/\<\?xml[^\?].*\?\>/','',$data); // Create a fake feed wrapper so simplepie doesn't choke - $tpl = load_view_file('view/atom_feed.tpl'); + $tpl = load_view_file('view/fake_feed.tpl'); - $base = substr($xml,strpos($xml,''; + $feedxml = $tpl . $base . ''; -salmon_return(500); // until the handler is finished + if($debugging) { + file_put_contents('salmon.out', 'Processed feed: ' . $feedxml . "\n", FILE_APPEND); + } -// consume_salmon($xml,$importer); + // Now parse it like a normal atom feed to scrape out the author URI + + $feed = new SimplePie(); + $feed->set_raw_data($feedxml); + $feed->enable_order_by_date(false); + $feed->init(); + + if($debugging) { + file_put_contents('salmon.out', "\n" . 'Feed parsed.' . "\n", FILE_APPEND); + } + + + if($feed->get_item_quantity()) { + foreach($feed->get_items() as $item) { + $author = $item->get_author(); + $author_link = unxmlify($author->get_link()); + break; + } + } + + if(! $author_link) { + if($debugging) + file_put_contents('salmon.out',"\n" . 'Could not retrieve author URI.' . "\n", FILE_APPEND); + salmon_return(500); + } + + // Once we have the author URI, go to the web and find their public key + + if($debugging) { + file_put_contents('salmon.out', "\n" . 'Fetching key for ' . $author_link . "\n", FILE_APPEND); + } + + $key = get_salmon_key($author_link,$keyhash); + + if(! $key) { + if($debugging) + file_put_contents('salmon.out',"\n" . 'Could not retrieve author key.' . "\n", FILE_APPEND); + salmon_return(500); + } + + // Setup RSA stuff to verify the signature + + set_include_path(get_include_path() . PATH_SEPARATOR . 'phpsec'); + + require_once('phpsec/Crypt/RSA.php'); + + $key_info = explode('.',$key); + + $m = base64url_decode($key_info[1]); + $e = base64url_decode($key_info[2]); + if($debugging) + file_put_contents('salmon.out',"\n" . print_r($key_info,true) . "\n", FILE_APPEND); + + $rsa = new CRYPT_RSA(); + $rsa->signatureMode = CRYPT_RSA_SIGNATURE_PKCS1; + $rsa->setHash('sha256'); + + $rsa->modulus = new Math_BigInteger($m, 256); + $rsa->k = strlen($rsa->modulus->toBytes()); + $rsa->exponent = new Math_BigInteger($e, 256); + + // We should have everything we need now. Let's see if it verifies. + + $verify = $rsa->verify($signed_data,$signature); + + if(! $verify) { + if($debugging) + file_put_contents('salmon.out',"\n" . 'Message did not verify. Discarding.' . "\n", FILE_APPEND); + salmon_return(500); + } + + if($debugging) + file_put_contents('salmon.out',"\n" . 'Message verified.' . "\n", FILE_APPEND); + + + /* + * + * If we reached this point, the message is good. Now let's figure out if the author is allowed to send us stuff. + * + */ + + $r = q("SELECT * FROM `contact` WHERE `network` = 'stat' AND `lrdd` = '%s' AND `uid` = %d LIMIT 1", + dbesc($author_link), + intval($importer['uid']) + ); + if(! count($r)) { + if($debugging) + file_put_contents('salmon.out',"\n" . 'Author unknown to us.' . "\n", FILE_APPEND); + salmon_return(500); + } + + + require_once('include/items.php'); + + $hub = ''; + + consume_feed($feedxml,$importer,$r[0],$hub); salmon_return(200); } + diff --git a/update.php b/update.php index 3785cad2b2..65713583b3 100644 --- a/update.php +++ b/update.php @@ -75,4 +75,8 @@ function update_1008() { function update_1009() { q("ALTER TABLE `user` ADD `allow_location` TINYINT( 1 ) NOT NULL DEFAULT '0' AFTER `default-location` "); -} \ No newline at end of file +} + +function update_1010() { + q("ALTER TABLE `contact` ADD `lrdd` CHAR( 255 ) NOT NULL AFTER `url` "); +} diff --git a/view/fake_feed.tpl b/view/fake_feed.tpl new file mode 100644 index 0000000000..49b17e34da --- /dev/null +++ b/view/fake_feed.tpl @@ -0,0 +1,18 @@ + + + + fake feed + fake title + + 1970-01-01T00:00:00Z + + + Fake Name + http://example.com +