diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index d47419a41..49df14762 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -8,6 +8,7 @@ use Friendica\BaseModule; use Friendica\Protocol\ActivityPub; use Friendica\Core\System; use Friendica\Database\DBA; +use Friendica\Util\HTTPSignature; /** * ActivityPub Inbox @@ -24,7 +25,7 @@ class Inbox extends BaseModule System::httpExit(400); } - if (ActivityPub::verifySignature($postdata, $_SERVER)) { + if (HTTPSignature::verifyAP($postdata, $_SERVER)) { $filename = 'signed-activitypub'; } else { $filename = 'failed-activitypub'; diff --git a/src/Module/Magic.php b/src/Module/Magic.php index cf77482e5..0b4126e0e 100644 --- a/src/Module/Magic.php +++ b/src/Module/Magic.php @@ -76,13 +76,9 @@ class Magic extends BaseModule // Create a header that is signed with the local users private key. $headers = HTTPSignature::createSig( - '', $headers, $user['prvkey'], - 'acct:' . $user['nickname'] . '@' . $a->get_hostname() . ($a->urlpath ? '/' . $a->urlpath : ''), - false, - true, - 'sha512' + 'acct:' . $user['nickname'] . '@' . $a->get_hostname() . ($a->urlpath ? '/' . $a->urlpath : '') ); // Try to get an authentication token from the other instance. diff --git a/src/Module/Owa.php b/src/Module/Owa.php index 1d6b1332d..68f31c59d 100644 --- a/src/Module/Owa.php +++ b/src/Module/Owa.php @@ -54,7 +54,7 @@ class Owa extends BaseModule if (DBA::isResult($contact)) { // Try to verify the signed header with the public key of the contact record // we have found. - $verified = HTTPSignature::verify('', $contact['pubkey']); + $verified = HTTPSignature:verifyMagic($contact['pubkey']); if ($verified && $verified['header_signed'] && $verified['header_valid']) { logger('OWA header: ' . print_r($verified, true), LOGGER_DATA); diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 25ebedc8b..d952f4de8 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -63,38 +63,6 @@ class ActivityPub stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/ld+json'); } - public static function transmit($data, $target, $uid) - { - $owner = User::getOwnerDataById($uid); - - if (!$owner) { - return; - } - - $content = json_encode($data); - - // Header data that is about to be signed. - $host = parse_url($target, PHP_URL_HOST); - $path = parse_url($target, PHP_URL_PATH); - $digest = 'SHA-256=' . base64_encode(hash('sha256', $content, true)); - $content_length = strlen($content); - - $headers = ['Content-Length: ' . $content_length, 'Digest: ' . $digest, 'Host: ' . $host]; - - $signed_data = "(request-target): post " . $path . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host; - - $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); - - $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) content-length digest host",signature="' . $signature . '"'; - - $headers[] = 'Content-Type: application/activity+json'; - - Network::post($target, $content, $headers); - $return_code = BaseObject::getApp()->get_curl_code(); - - logger('Transmit to ' . $target . ' returned ' . $return_code); - } - /** * Return the ActivityPub profile of the given user * @@ -401,7 +369,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG); - return self::transmit($data, $profile['inbox'], $uid); + return HTTPSignature::transmit($data, $profile['inbox'], $uid); } public static function transmitContactAccept($target, $id, $uid) @@ -419,7 +387,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['inbox'], $uid); + return HTTPSignature::transmit($data, $profile['inbox'], $uid); } public static function transmitContactReject($target, $id, $uid) @@ -437,7 +405,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['inbox'], $uid); + return HTTPSignature::transmit($data, $profile['inbox'], $uid); } public static function transmitContactUndo($target, $uid) @@ -457,7 +425,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['inbox'], $uid); + return HTTPSignature::transmit($data, $profile['inbox'], $uid); } /** @@ -515,154 +483,6 @@ class ActivityPub return false; } - public static function verifySignature($content, $http_headers) - { - $object = json_decode($content, true); - - if (empty($object)) { - return false; - } - - $actor = JsonLD::fetchElement($object, 'actor', 'id'); - - $headers = []; - $headers['(request-target)'] = strtolower($http_headers['REQUEST_METHOD']) . ' ' . $http_headers['REQUEST_URI']; - - // First take every header - foreach ($http_headers as $k => $v) { - $field = str_replace('_', '-', strtolower($k)); - $headers[$field] = $v; - } - - // Now add every http header - foreach ($http_headers as $k => $v) { - if (strpos($k, 'HTTP_') === 0) { - $field = str_replace('_', '-', strtolower(substr($k, 5))); - $headers[$field] = $v; - } - } - - $sig_block = ActivityPub::parseSigHeader($http_headers['HTTP_SIGNATURE']); - - if (empty($sig_block) || empty($sig_block['headers']) || empty($sig_block['keyId'])) { - return false; - } - - $signed_data = ''; - foreach ($sig_block['headers'] as $h) { - if (array_key_exists($h, $headers)) { - $signed_data .= $h . ': ' . $headers[$h] . "\n"; - } - } - $signed_data = rtrim($signed_data, "\n"); - - if (empty($signed_data)) { - return false; - } - - $algorithm = null; - - if ($sig_block['algorithm'] === 'rsa-sha256') { - $algorithm = 'sha256'; - } - - if ($sig_block['algorithm'] === 'rsa-sha512') { - $algorithm = 'sha512'; - } - - if (empty($algorithm)) { - return false; - } - - $key = self::fetchKey($sig_block['keyId'], $actor); - - if (empty($key)) { - return false; - } - - if (!Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm)) { - return false; - } - - // Check the digest when it is part of the signed data - if (in_array('digest', $sig_block['headers'])) { - $digest = explode('=', $headers['digest'], 2); - if ($digest[0] === 'SHA-256') { - $hashalg = 'sha256'; - } - if ($digest[0] === 'SHA-512') { - $hashalg = 'sha512'; - } - - /// @todo add all hashes from the rfc - - if (!empty($hashalg) && base64_encode(hash($hashalg, $content, true)) != $digest[1]) { - return false; - } - } - - // Check the content-length when it is part of the signed data - if (in_array('content-length', $sig_block['headers'])) { - if (strlen($content) != $headers['content-length']) { - return false; - } - } - - return true; - - } - - private static function fetchKey($id, $actor) - { - $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); - - $profile = self::fetchprofile($url); - if (!empty($profile)) { - return $profile['pubkey']; - } elseif ($url != $actor) { - $profile = self::fetchprofile($actor); - if (!empty($profile)) { - return $profile['pubkey']; - } - } - - return false; - } - - /** - * @brief - * - * @param string $header - * @return array associate array with - * - \e string \b keyID - * - \e string \b algorithm - * - \e array \b headers - * - \e string \b signature - */ - private static function parseSigHeader($header) - { - $ret = []; - $matches = []; - - if (preg_match('/keyId="(.*?)"/ism',$header,$matches)) { - $ret['keyId'] = $matches[1]; - } - - if (preg_match('/algorithm="(.*?)"/ism',$header,$matches)) { - $ret['algorithm'] = $matches[1]; - } - - if (preg_match('/headers="(.*?)"/ism',$header,$matches)) { - $ret['headers'] = explode(' ', $matches[1]); - } - - if (preg_match('/signature="(.*?)"/ism',$header,$matches)) { - $ret['signature'] = base64_decode(preg_replace('/\s+/','',$matches[1])); - } - - return $ret; - } - public static function fetchprofile($url, $update = false) { if (empty($url)) { @@ -798,7 +618,7 @@ class ActivityPub { logger('Incoming message for user ' . $uid, LOGGER_DEBUG); - if (!self::verifySignature($body, $header)) { + if (!HTTPSignature::verifyAP($body, $header)) { logger('Invalid signature, message will be discarded.', LOGGER_DEBUG); return; } @@ -813,7 +633,7 @@ class ActivityPub self::processActivity($activity, $body, $uid); } - public static function fetchOutbox($url) + public static function fetchOutbox($url, $uid) { $data = self::fetchContent($url); if (empty($data)) { @@ -825,14 +645,14 @@ class ActivityPub } elseif (!empty($data['first']['orderedItems'])) { $items = $data['first']['orderedItems']; } elseif (!empty($data['first'])) { - self::fetchOutbox($data['first']); + self::fetchOutbox($data['first'], $uid); return; } else { $items = []; } foreach ($items as $activity) { - self::processActivity($activity); + self::processActivity($activity, '', $uid); } } diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index adf5d8ad2..f6a5fe1fe 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -5,8 +5,11 @@ */ namespace Friendica\Util; +use Friendica\BaseObject; use Friendica\Core\Config; use Friendica\Database\DBA; +use Friendica\Model\User; +use Friendica\Protocol\ActivityPub; /** * @brief Implements HTTP Signatures per draft-cavage-http-signatures-07. @@ -19,56 +22,36 @@ use Friendica\Database\DBA; class HTTPSignature { // See draft-cavage-http-signatures-08 - public static function verify($data, $key = '') + public static function verifyMagic($key) { - $body = $data; $headers = null; $spoofable = false; $result = [ 'signer' => '', 'header_signed' => false, - 'header_valid' => false, - 'content_signed' => false, - 'content_valid' => false + 'header_valid' => false ]; // Decide if $data arrived via controller submission or curl. - if (is_array($data) && $data['header']) { - if (!$data['success']) { - return $result; - } + $headers = []; + $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI']; - $h = new HTTPHeaders($data['header']); - $headers = $h->fetch(); - $body = $data['body']; - } else { - $headers = []; - $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI']; - - foreach ($_SERVER as $k => $v) { - if (strpos($k, 'HTTP_') === 0) { - $field = str_replace('_', '-', strtolower(substr($k, 5))); - $headers[$field] = $v; - } + foreach ($_SERVER as $k => $v) { + if (strpos($k, 'HTTP_') === 0) { + $field = str_replace('_', '-', strtolower(substr($k, 5))); + $headers[$field] = $v; } } $sig_block = null; - if (array_key_exists('signature', $headers)) { - $sig_block = self::parseSigheader($headers['signature']); - } elseif (array_key_exists('authorization', $headers)) { - $sig_block = self::parseSigheader($headers['authorization']); - } + $sig_block = self::parseSigheader($headers['authorization']); if (!$sig_block) { logger('no signature provided.'); return $result; } - // Warning: This log statement includes binary data - // logger('sig_block: ' . print_r($sig_block,true), LOGGER_DATA); - $result['header_signed'] = true; $signed_headers = $sig_block['headers']; @@ -88,13 +71,7 @@ class HTTPSignature $signed_data = rtrim($signed_data, "\n"); - $algorithm = null; - if ($sig_block['algorithm'] === 'rsa-sha256') { - $algorithm = 'sha256'; - } - if ($sig_block['algorithm'] === 'rsa-sha512') { - $algorithm = 'sha512'; - } + $algorithm = 'sha512'; if ($key && function_exists($key)) { $result['signer'] = $sig_block['keyId']; @@ -119,93 +96,39 @@ class HTTPSignature $result['header_valid'] = true; } - if (in_array('digest', $signed_headers)) { - $result['content_signed'] = true; - $digest = explode('=', $headers['digest']); - - if ($digest[0] === 'SHA-256') { - $hashalg = 'sha256'; - } - if ($digest[0] === 'SHA-512') { - $hashalg = 'sha512'; - } - - // The explode operation will have stripped the '=' padding, so compare against unpadded base64. - if (rtrim(base64_encode(hash($hashalg, $body, true)), '=') === $digest[1]) { - $result['content_valid'] = true; - } - } - - logger('Content_Valid: ' . $result['content_valid']); - return $result; } /** * @brief * - * @param string $request * @param array $head * @param string $prvkey * @param string $keyid (optional, default 'Key') - * @param boolean $send_headers (optional, default false) - * If set send a HTTP header - * @param boolean $auth (optional, default false) - * @param string $alg (optional, default 'sha256') - * @param string $crypt_key (optional, default null) - * @param string $crypt_algo (optional, default 'aes256ctr') * * @return array */ - public static function createSig($request, $head, $prvkey, $keyid = 'Key', $send_headers = false, $auth = false, $alg = 'sha256', $crypt_key = null, $crypt_algo = 'aes256ctr') + public static function createSig($head, $prvkey, $keyid = 'Key') { $return_headers = []; - if ($alg === 'sha256') { - $algorithm = 'rsa-sha256'; - } + $alg = 'sha512'; + $algorithm = 'rsa-sha512'; - if ($alg === 'sha512') { - $algorithm = 'rsa-sha512'; - } - - $x = self::sign($request, $head, $prvkey, $alg); + $x = self::sign($head, $prvkey, $alg); $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"'; - if ($crypt_key) { - $x = Crypto::encapsulate($headerval, $crypt_key, $crypt_algo); - $headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"'; - } - - if ($auth) { - $sighead = 'Authorization: Signature ' . $headerval; - } else { - $sighead = 'Signature: ' . $headerval; - } + $sighead = 'Authorization: Signature ' . $headerval; if ($head) { foreach ($head as $k => $v) { - if ($send_headers) { - // This is for ActivityPub implementation. - // Since the Activity Pub implementation isn't - // ready at the moment, we comment it out. - // header($k . ': ' . $v); - } else { - $return_headers[] = $k . ': ' . $v; - } + $return_headers[] = $k . ': ' . $v; } } - if ($send_headers) { - // This is for ActivityPub implementation. - // Since the Activity Pub implementation isn't - // ready at the moment, we comment it out. - // header($sighead); - } else { - $return_headers[] = $sighead; - } + $return_headers[] = $sighead; return $return_headers; } @@ -213,35 +136,27 @@ class HTTPSignature /** * @brief * - * @param string $request * @param array $head * @param string $prvkey * @param string $alg (optional) default 'sha256' * * @return array */ - private static function sign($request, $head, $prvkey, $alg = 'sha256') + private static function sign($head, $prvkey, $alg = 'sha256') { $ret = []; $headers = ''; $fields = ''; - if ($request) { - $headers = '(request-target)' . ': ' . trim($request) . "\n"; - $fields = '(request-target)'; - } - - if ($head) { - foreach ($head as $k => $v) { - $headers .= strtolower($k) . ': ' . trim($v) . "\n"; - if ($fields) { - $fields .= ' '; - } - $fields .= strtolower($k); + foreach ($head as $k => $v) { + $headers .= strtolower($k) . ': ' . trim($v) . "\n"; + if ($fields) { + $fields .= ' '; } - // strip the trailing linefeed - $headers = rtrim($headers, "\n"); + $fields .= strtolower($k); } + // strip the trailing linefeed + $headers = rtrim($headers, "\n"); $sig = base64_encode(Crypto::rsaSign($headers, $prvkey, $alg)); @@ -338,4 +253,154 @@ class HTTPSignature return ''; } + + /** + * Functions for ActivityPub + */ + + public static function transmit($data, $target, $uid) + { + $owner = User::getOwnerDataById($uid); + + if (!$owner) { + return; + } + + $content = json_encode($data); + + // Header data that is about to be signed. + $host = parse_url($target, PHP_URL_HOST); + $path = parse_url($target, PHP_URL_PATH); + $digest = 'SHA-256=' . base64_encode(hash('sha256', $content, true)); + $content_length = strlen($content); + + $headers = ['Content-Length: ' . $content_length, 'Digest: ' . $digest, 'Host: ' . $host]; + + $signed_data = "(request-target): post " . $path . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host; + + $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); + + $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) content-length digest host",signature="' . $signature . '"'; + + $headers[] = 'Content-Type: application/activity+json'; + + Network::post($target, $content, $headers); + $return_code = BaseObject::getApp()->get_curl_code(); + + logger('Transmit to ' . $target . ' returned ' . $return_code); + } + + public static function verifyAP($content, $http_headers) + { + $object = json_decode($content, true); + + if (empty($object)) { + return false; + } + + $actor = JsonLD::fetchElement($object, 'actor', 'id'); + + $headers = []; + $headers['(request-target)'] = strtolower($http_headers['REQUEST_METHOD']) . ' ' . $http_headers['REQUEST_URI']; + + // First take every header + foreach ($http_headers as $k => $v) { + $field = str_replace('_', '-', strtolower($k)); + $headers[$field] = $v; + } + + // Now add every http header + foreach ($http_headers as $k => $v) { + if (strpos($k, 'HTTP_') === 0) { + $field = str_replace('_', '-', strtolower(substr($k, 5))); + $headers[$field] = $v; + } + } + + $sig_block = self::parseSigHeader($http_headers['HTTP_SIGNATURE']); + + if (empty($sig_block) || empty($sig_block['headers']) || empty($sig_block['keyId'])) { + return false; + } + + $signed_data = ''; + foreach ($sig_block['headers'] as $h) { + if (array_key_exists($h, $headers)) { + $signed_data .= $h . ': ' . $headers[$h] . "\n"; + } + } + $signed_data = rtrim($signed_data, "\n"); + + if (empty($signed_data)) { + return false; + } + + $algorithm = null; + + if ($sig_block['algorithm'] === 'rsa-sha256') { + $algorithm = 'sha256'; + } + + if ($sig_block['algorithm'] === 'rsa-sha512') { + $algorithm = 'sha512'; + } + + if (empty($algorithm)) { + return false; + } + + $key = self::fetchKey($sig_block['keyId'], $actor); + + if (empty($key)) { + return false; + } + + if (!Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm)) { + return false; + } + + // Check the digest when it is part of the signed data + if (in_array('digest', $sig_block['headers'])) { + $digest = explode('=', $headers['digest'], 2); + if ($digest[0] === 'SHA-256') { + $hashalg = 'sha256'; + } + if ($digest[0] === 'SHA-512') { + $hashalg = 'sha512'; + } + + /// @todo add all hashes from the rfc + + if (!empty($hashalg) && base64_encode(hash($hashalg, $content, true)) != $digest[1]) { + return false; + } + } + + // Check the content-length when it is part of the signed data + if (in_array('content-length', $sig_block['headers'])) { + if (strlen($content) != $headers['content-length']) { + return false; + } + } + + return true; + + } + + private static function fetchKey($id, $actor) + { + $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); + + $profile = ActivityPub::fetchprofile($url); + if (!empty($profile)) { + return $profile['pubkey']; + } elseif ($url != $actor) { + $profile = ActivityPub::fetchprofile($actor); + if (!empty($profile)) { + return $profile['pubkey']; + } + } + + return false; + } } diff --git a/src/Worker/APDelivery.php b/src/Worker/APDelivery.php index b7e881c7a..f43c56a3e 100644 --- a/src/Worker/APDelivery.php +++ b/src/Worker/APDelivery.php @@ -7,6 +7,7 @@ namespace Friendica\Worker; use Friendica\BaseObject; use Friendica\Protocol\ActivityPub; use Friendica\Model\Item; +use Friendica\Util\HTTPSignature; class APDelivery extends BaseObject { @@ -20,7 +21,7 @@ class APDelivery extends BaseObject } else { $item = Item::selectFirst(['uid'], ['id' => $item_id]); $data = ActivityPub::createActivityFromItem($item_id); - ActivityPub::transmit($data, $inbox, $item['uid']); + HTTPSignature::transmit($data, $inbox, $item['uid']); } return;