public function onEndAttachPubkeyToUserXRD(Magicsig $magicsig, XML_XRD $xrd, Profile $target)
 {
     // So far we've only handled RSA keys, but it can change in the future,
     // so be prepared. And remember to change the statically assigned type attribute below!
     assert($magicsig->publicKey instanceof Crypt_RSA);
     $xrd->links[] = new XML_XRD_Element_Link(self::REL_PUBLIC_KEY, base64_encode($magicsig->exportPublicKey()), 'RSA');
     // Instead of choosing a random string, we calculate our GUID from the public key
     // by fingerprint through a sha256 hash.
     $xrd->links[] = new XML_XRD_Element_Link(self::REL_GUID, strtolower($magicsig->toFingerprint()));
 }
 function handle()
 {
     $nick = $this->user->nickname;
     $profile = $this->user->getProfile();
     if (empty($this->xrd)) {
         $xrd = new XRD();
     } else {
         $xrd = $this->xrd;
     }
     if (empty($xrd->subject)) {
         $xrd->subject = Discovery::normalize($this->uri);
     }
     // Possible aliases for the user
     $uris = array($this->user->uri, $profile->profileurl);
     // FIXME: Webfinger generation code should live somewhere on its own
     $path = common_config('site', 'path');
     if (empty($path)) {
         $uris[] = sprintf('acct:%s@%s', $nick, common_config('site', 'server'));
     }
     foreach ($uris as $uri) {
         if ($uri != $xrd->subject) {
             $xrd->alias[] = $uri;
         }
     }
     $xrd->links[] = array('rel' => Discovery::PROFILEPAGE, 'type' => 'text/html', 'href' => $profile->profileurl);
     $xrd->links[] = array('rel' => Discovery::UPDATESFROM, 'href' => common_local_url('ApiTimelineUser', array('id' => $this->user->id, 'format' => 'atom')), 'type' => 'application/atom+xml');
     // hCard
     $xrd->links[] = array('rel' => Discovery::HCARD, 'type' => 'text/html', 'href' => common_local_url('hcard', array('nickname' => $nick)));
     // XFN
     $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', 'type' => 'text/html', 'href' => $profile->profileurl);
     // FOAF
     $xrd->links[] = array('rel' => 'describedby', 'type' => 'application/rdf+xml', 'href' => common_local_url('foaf', array('nickname' => $nick)));
     // Salmon
     $salmon_url = common_local_url('usersalmon', array('id' => $this->user->id));
     $xrd->links[] = array('rel' => Salmon::REL_SALMON, 'href' => $salmon_url);
     // XXX : Deprecated - to be removed.
     $xrd->links[] = array('rel' => Salmon::NS_REPLIES, 'href' => $salmon_url);
     $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, 'href' => $salmon_url);
     // Get this user's keypair
     $magickey = Magicsig::staticGet('user_id', $this->user->id);
     if (!$magickey) {
         // No keypair yet, let's generate one.
         $magickey = new Magicsig();
         $magickey->generate($this->user->id);
     }
     $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, 'href' => 'data:application/magic-public-key,' . $magickey->toString(false));
     // TODO - finalize where the redirect should go on the publisher
     $url = common_local_url('ostatussub') . '?profile={uri}';
     $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => $url);
     $url = common_local_url('tagprofile') . '?uri={uri}';
     $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/tag', 'template' => $url);
     header('Content-type: application/xrd+xml');
     print $xrd->toXML();
 }
 public function createMagicEnv($text, $actor)
 {
     $magic_env = new MagicEnvelope();
     $user = User::staticGet('id', $actor->id);
     if ($user->id) {
         // Use local key
         $magickey = Magicsig::staticGet('user_id', $user->id);
         if (!$magickey) {
             // No keypair yet, let's generate one.
             $magickey = new Magicsig();
             $magickey->generate($user->id);
         }
     } else {
         throw new Exception("Salmon invalid actor for signing");
     }
     try {
         $env = $magic_env->signMessage($text, 'application/atom+xml', $magickey->toString());
     } catch (Exception $e) {
         return $text;
     }
     return $magic_env->toXML($env);
 }
Exemple #4
0
 function handle()
 {
     $nick = $this->user->nickname;
     if (empty($this->xrd)) {
         $xrd = new XRD();
     } else {
         $xrd = $this->xrd;
     }
     if (empty($xrd->subject)) {
         $xrd->subject = Discovery::normalize($this->uri);
     }
     $xrd->alias[] = $this->user->uri;
     $xrd->links[] = array('rel' => Discovery::PROFILEPAGE, 'type' => 'text/html', 'href' => $this->user->uri);
     $xrd->links[] = array('rel' => Discovery::UPDATESFROM, 'href' => common_local_url('ApiTimelineUser', array('id' => $this->user->id, 'format' => 'atom')), 'type' => 'application/atom+xml');
     // hCard
     $xrd->links[] = array('rel' => Discovery::HCARD, 'type' => 'text/html', 'href' => common_local_url('hcard', array('nickname' => $nick)));
     // XFN
     $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', 'type' => 'text/html', 'href' => $this->user->uri);
     // FOAF
     $xrd->links[] = array('rel' => 'describedby', 'type' => 'application/rdf+xml', 'href' => common_local_url('foaf', array('nickname' => $nick)));
     // Salmon
     $salmon_url = common_local_url('usersalmon', array('id' => $this->user->id));
     $xrd->links[] = array('rel' => Salmon::NS_REPLIES, 'href' => $salmon_url);
     $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, 'href' => $salmon_url);
     // Get this user's keypair
     $magickey = Magicsig::staticGet('user_id', $this->user->id);
     if (!$magickey) {
         // No keypair yet, let's generate one.
         $magickey = new Magicsig();
         $magickey->generate($this->user->id);
     }
     $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, 'href' => 'data:application/magic-public-key,' . $magickey->toString(false));
     // TODO - finalize where the redirect should go on the publisher
     $url = common_local_url('ostatussub') . '?profile={uri}';
     $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => $url);
     header('Content-type: text/xml');
     print $xrd->toXML();
 }
 protected function prepare(array $args = array())
 {
     GNUsocial::setApi(true);
     // Send smaller error pages
     parent::prepare($args);
     if (!isset($_SERVER['CONTENT_TYPE'])) {
         // TRANS: Client error. Do not translate "Content-type"
         $this->clientError(_m('Salmon requires a Content-type header.'));
     }
     $envxml = null;
     switch ($_SERVER['CONTENT_TYPE']) {
         case 'application/magic-envelope+xml':
             $envxml = file_get_contents('php://input');
             break;
         case 'application/x-www-form-urlencoded':
             $envxml = Magicsig::base64_url_decode($this->trimmed('xml'));
             break;
         default:
             // TRANS: Client error. Do not translate the quoted "application/[type]" strings.
             $this->clientError(_m('Salmon requires "application/magic-envelope+xml". For Diaspora we also accept "application/x-www-form-urlencoded" with an "xml" parameter.', 415));
     }
     try {
         if (empty($envxml)) {
             throw new ClientException('No magic envelope supplied in POST.');
         }
         $magic_env = new MagicEnvelope($envxml);
         // parse incoming XML as a MagicEnvelope
         $entry = $magic_env->getPayload();
         // Not cryptographically verified yet!
         $this->activity = new Activity($entry->documentElement);
         if (empty($this->activity->actor->id)) {
             common_log(LOG_ERR, "broken actor: " . var_export($this->activity->actor->id, true));
             common_log(LOG_ERR, "activity with no actor: " . var_export($this->activity, true));
             // TRANS: Exception.
             throw new Exception(_m('Received a salmon slap from unidentified actor.'));
         }
         // ensureProfiles sets $this->actor and $this->oprofile
         $this->ensureProfiles();
     } catch (Exception $e) {
         common_debug('Salmon envelope parsing failed with: ' . $e->getMessage());
         $this->clientError($e->getMessage());
     }
     // Cryptographic verification test
     if (!$magic_env->verify($this->actor)) {
         common_log(LOG_DEBUG, "Salmon signature verification failed.");
         // TRANS: Client error.
         $this->clientError(_m('Salmon signature verification failed.'));
     }
     return true;
 }
Exemple #6
0
 /**
  * Sign and post the given Atom entry as a Salmon message.
  *
  * Side effects: may generate a keypair on-demand for the given user,
  * which can be very slow on some systems (like those without php5-gmp).
  *
  * @param string $endpoint_uri
  * @param string $xml string representation of payload
  * @param User $user local user profile whose keys we sign with
  * @return boolean success
  */
 public static function post($endpoint_uri, $xml, User $user)
 {
     if (empty($endpoint_uri)) {
         common_debug('No endpoint URI for Salmon post to ' . $user->getUri());
         return false;
     }
     try {
         $magic_env = MagicEnvelope::signAsUser($xml, $user);
         $envxml = $magic_env->toXML();
     } catch (Exception $e) {
         common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
         return false;
     }
     $headers = array('Content-Type: application/magic-envelope+xml');
     try {
         $client = new HTTPClient();
         $client->setBody($envxml);
         $response = $client->post($endpoint_uri, $headers);
     } catch (HTTP_Request2_Exception $e) {
         common_log(LOG_ERR, "Salmon post to {$endpoint_uri} failed: " . $e->getMessage());
         return false;
     }
     // Diaspora wants a slightly different formatting on the POST (other Content-type, so body needs "xml=")
     if ($response->getStatus() === 422) {
         common_debug(sprintf('Salmon (from profile %d) endpoint %s returned status %s. Diaspora? Will try again! Body: %s', $user->id, $endpoint_uri, $response->getStatus(), $response->getBody()));
         $headers = array('Content-Type: application/x-www-form-urlencoded');
         $client->setBody('xml=' . Magicsig::base64_url_encode($envxml));
         $response = $client->post($endpoint_uri, $headers);
     }
     // 200 OK is the best response
     // 202 Accepted is what we get from Diaspora for example
     if (!in_array($response->getStatus(), array(200, 202))) {
         common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s', $user->id, $endpoint_uri, $response->getStatus(), $response->getBody()));
         return false;
     }
     // Success!
     return true;
 }
Exemple #7
0
 /**
  *
  * @param string $signed_bytes as raw byte string
  * @param string $signature as base64
  * @return boolean
  */
 public function verify($signed_bytes, $signature)
 {
     $signature = Magicsig::base64_url_decode($signature);
     return $this->publicKey->verify($signed_bytes, $signature);
 }
 public function verify($env)
 {
     if ($env['alg'] != 'RSA-SHA256') {
         common_log(LOG_DEBUG, "Salmon error: bad algorithm");
         return false;
     }
     if ($env['encoding'] != MagicEnvelope::ENCODING) {
         common_log(LOG_DEBUG, "Salmon error: bad encoding");
         return false;
     }
     $text = Magicsig::base64_url_decode($env['data']);
     $signer_uri = $this->getAuthor($text);
     try {
         $keypair = $this->getKeyPair($signer_uri);
     } catch (Exception $e) {
         common_log(LOG_DEBUG, "Salmon error: " . $e->getMessage());
         return false;
     }
     $verifier = Magicsig::fromString($keypair);
     if (!$verifier) {
         common_log(LOG_DEBUG, "Salmon error: unable to parse keypair");
         return false;
     }
     return $verifier->verify($env['data'], $env['sig']);
 }
 /**
  * Encode the given string as a signed MagicEnvelope XML document,
  * using the keypair for the given local user profile. We can of
  * course not sign a remote profile's slap, since we don't have the
  * private key.
  *
  * Side effects: will create and store a keypair on-demand if one
  * hasn't already been generated for this user. This can be very slow
  * on some systems.
  *
  * @param string $text XML fragment to sign, assumed to be Atom
  * @param User $user User who cryptographically signs $text
  *
  * @return MagicEnvelope object complete with signature
  *
  * @throws Exception on bad profile input or key generation problems
  */
 public static function signAsUser($text, User $user)
 {
     // Find already stored key
     $magicsig = Magicsig::getKV('user_id', $user->id);
     if (!$magicsig instanceof Magicsig) {
         $magicsig = Magicsig::generate($user);
     }
     assert($magicsig instanceof Magicsig);
     assert($magicsig->privateKey instanceof Crypt_RSA);
     $magic_env = new MagicEnvelope();
     $magic_env->signMessage($text, 'application/atom+xml', $magicsig);
     return $magic_env;
 }
 function onEndXrdActionLinks(&$xrd, $user)
 {
     $xrd->links[] = array('rel' => Discovery::UPDATESFROM, 'href' => common_local_url('ApiTimelineUser', array('id' => $user->id, 'format' => 'atom')), 'type' => 'application/atom+xml');
     // Salmon
     $salmon_url = common_local_url('usersalmon', array('id' => $user->id));
     $xrd->links[] = array('rel' => Salmon::REL_SALMON, 'href' => $salmon_url);
     // XXX : Deprecated - to be removed.
     $xrd->links[] = array('rel' => Salmon::NS_REPLIES, 'href' => $salmon_url);
     $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, 'href' => $salmon_url);
     // Get this user's keypair
     $magickey = Magicsig::staticGet('user_id', $user->id);
     if (!$magickey) {
         // No keypair yet, let's generate one.
         $magickey = new Magicsig();
         $magickey->generate($user->id);
     }
     $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, 'href' => 'data:application/magic-public-key,' . $magickey->toString(false));
     // TODO - finalize where the redirect should go on the publisher
     $url = common_local_url('ostatussub') . '?profile={uri}';
     $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => $url);
     return true;
 }
 /**
  * Make sure necessary tables are filled out.
  */
 function onCheckSchema()
 {
     $schema = Schema::get();
     $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
     $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
     $schema->ensureTable('feedsub', FeedSub::schemaDef());
     $schema->ensureTable('hubsub', HubSub::schemaDef());
     $schema->ensureTable('magicsig', Magicsig::schemaDef());
     return true;
 }
Exemple #12
0
 /**
  * Generate base64-encoded signature for the given byte string
  * using our private key.
  *
  * @param string $bytes as raw byte string
  * @return string base64url-encoded signature
  */
 public function sign($bytes)
 {
     $sig = $this->privateKey->sign($bytes);
     if ($sig === false) {
         throw new ServerException('Could not sign data');
     }
     return Magicsig::base64_url_encode($sig);
 }
 public function getPayload()
 {
     $dom = new DOMDocument();
     if (!$dom->loadXML(Magicsig::base64_url_decode($this->data))) {
         throw new ServerException('Malformed XML in Salmon payload');
     }
     switch ($this->data_type) {
         case 'application/atom+xml':
             if ($dom->documentElement->namespaceURI !== Activity::ATOM || $dom->documentElement->tagName !== 'entry') {
                 throw new ServerException(_m('Salmon post must be an Atom entry.'));
             }
             $prov = $dom->createElementNS(self::NS, 'me:provenance');
             $prov->setAttribute('xmlns:me', self::NS);
             $data = $dom->createElementNS(self::NS, 'me:data', $this->data);
             $data->setAttribute('type', $this->data_type);
             $prov->appendChild($data);
             $enc = $dom->createElementNS(self::NS, 'me:encoding', $this->encoding);
             $prov->appendChild($enc);
             $alg = $dom->createElementNS(self::NS, 'me:alg', $this->alg);
             $prov->appendChild($alg);
             $sig = $dom->createElementNS(self::NS, 'me:sig', $this->getSignature());
             $prov->appendChild($sig);
             $dom->documentElement->appendChild($prov);
             break;
         default:
             throw new ServerException('Unknown Salmon payload data type');
     }
     return $dom;
 }
 public function onProfileDeleteRelated($profile, &$related)
 {
     // Ostatus_profile has a 'profile_id' property, which will be used to find the object
     $related[] = 'Ostatus_profile';
     // Magicsig has a "user_id" column instead, so we have to delete it more manually:
     $magicsig = Magicsig::getKV('user_id', $profile->id);
     if ($magicsig instanceof Magicsig) {
         $magicsig->delete();
     }
     return true;
 }
 public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target = null)
 {
     $envxml = $magic_env->toXML($target, 'diaspora');
     // Diaspora wants another POST format (base64url-encoded POST variable 'xml')
     $headers = array('Content-Type: application/x-www-form-urlencoded');
     // Another way to distinguish Diaspora from GNU social is that a POST with
     // $headers=array('Content-Type: application/magic-envelope+xml') would return
     // HTTP status code 422 Unprocessable Entity, at least as of 2015-10-04.
     try {
         $client = new HTTPClient();
         $client->setBody('xml=' . Magicsig::base64_url_encode($envxml));
         $response = $client->post($endpoint_uri, $headers);
     } catch (HTTP_Request2_Exception $e) {
         common_log(LOG_ERR, "Diaspora-flavoured Salmon post to {$endpoint_uri} failed: " . $e->getMessage());
         return false;
     }
     // 200 OK is the best response
     // 202 Accepted is what we get from Diaspora for example
     if (!in_array($response->getStatus(), array(200, 202))) {
         common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s', $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus(), $response->getBody()));
         return true;
     }
     // Success!
     return false;
 }