X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FUtil%2FHTTPSignature.php;h=408f4d978941e21bf42d023bef0b5a569e266cf3;hb=0a9dba005c0c70302e37ab4ce7f9598984ec9953;hp=c3a45205c37bd9326ada56d9955e8494df79312f;hpb=4e44b07dfe07dde78fe8a095fbb43d9ec1ffcb53;p=friendica.git diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index c3a45205c3..408f4d9789 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -5,95 +5,65 @@ */ namespace Friendica\Util; +use Friendica\BaseObject; use Friendica\Core\Config; -use Friendica\Database\dba; -use Friendica\Database\DBM; +use Friendica\Core\Logger; +use Friendica\Database\DBA; +use Friendica\Model\User; +use Friendica\Model\APContact; +use Friendica\Protocol\ActivityPub; /** * @brief Implements HTTP Signatures per draft-cavage-http-signatures-07. * * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/Zotlabs/Web/HTTPSig.php * + * Other parts of the code for HTTP signing are taken from the Osada project. + * https://framagit.org/macgirvin/osada + * * @see https://tools.ietf.org/html/draft-cavage-http-signatures-07 */ class HTTPSignature { + // See draft-cavage-http-signatures-08 /** - * @brief RFC5843 - * - * Disabled until Friendica's ActivityPub implementation - * is ready. + * @brief Verifies a magic request * - * @see https://tools.ietf.org/html/rfc5843 + * @param $key * - * @param string $body The value to create the digest for - * @param boolean $set (optional, default true) - * If set send a Digest HTTP header - * - * @return string The generated digest of $body + * @return array with verification data */ -// public static function generateDigest($body, $set = true) -// { -// $digest = base64_encode(hash('sha256', $body, true)); -// -// if($set) { -// header('Digest: SHA-256=' . $digest); -// } -// return $digest; -// } - - // 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.'); + Logger::log('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']; @@ -113,24 +83,14 @@ 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']; $key = $key($sig_block['keyId']); } - // We don't use Activity Pub at the moment. -// if (!$key) { -// $result['signer'] = $sig_block['keyId']; -// $key = self::getActivitypubKey($sig_block['keyId']); -// } + Logger::log('Got keyID ' . $sig_block['keyId']); if (!$key) { return $result; @@ -138,7 +98,7 @@ class HTTPSignature $x = Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm); - logger('verified: ' . $x, LOGGER_DEBUG); + Logger::log('verified: ' . $x, Logger::DEBUG); if (!$x) { return $result; @@ -148,130 +108,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; } - /** - * Fetch the public key for Activity Pub contact. - * - * @param string|int The identifier (contact addr or contact ID). - * @return string|boolean The public key or false on failure. - */ - private static function getActivitypubKey($id) - { - if (strpos($id, 'acct:') === 0) { - $contact = dba::selectFirst('contact', ['pubkey'], ['uid' => 0, 'addr' => str_replace('acct:', '', $id)]); - } else { - $contact = dba::selectFirst('contact', ['pubkey'], ['id' => $id, 'network' => 'activitypub']); - } - - if (DBM::is_result($contact)) { - return $contact['pubkey']; - } - - if(function_exists('as_fetch')) { - $r = as_fetch($id); - } - - if ($r) { - $j = json_decode($r, true); - - if (array_key_exists('publicKey', $j) && array_key_exists('publicKeyPem', $j['publicKey'])) { - if ((array_key_exists('id', $j['publicKey']) && $j['publicKey']['id'] !== $id) && $j['id'] !== $id) { - return false; - } - - return $j['publicKey']['publicKeyPem']; - } - } - - return false; - } - /** * @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; } @@ -279,35 +148,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)); @@ -404,4 +265,184 @@ class HTTPSignature return ''; } + + /* + * Functions for ActivityPub + */ + + /** + * @brief Transmit given data to a target for a user + * + * @param array $data Data that is about to be send + * @param string $target The URL of the inbox + * @param integer $uid User id of the sender + * + * @return boolean Was the transmission successful? + */ + public static function transmit($data, $target, $uid) + { + $owner = User::getOwnerDataById($uid); + + if (!$owner) { + return; + } + + $content = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + // 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'; + + $postResult = Network::post($target, $content, $headers); + $return_code = $postResult->getReturnCode(); + + Logger::log('Transmit to ' . $target . ' returned ' . $return_code); + + return ($return_code >= 200) && ($return_code <= 299); + } + + /** + * @brief Gets a signer from a given HTTP request + * + * @param $content + * @param $http_headers + * + * @return signer string + */ + public static function getSigner($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['pubkey'], $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; + } + } + + /// @todo Check if the signed date field is in an acceptable range + + // 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 $key['url']; + } + + /** + * @brief fetches a key for a given id and actor + * + * @param $id + * @param $actor + * + * @return array with actor url and public key + */ + private static function fetchKey($id, $actor) + { + $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); + + $profile = APContact::getByURL($url); + if (!empty($profile)) { + Logger::log('Taking key from id ' . $id, Logger::DEBUG); + return ['url' => $url, 'pubkey' => $profile['pubkey']]; + } elseif ($url != $actor) { + $profile = APContact::getByURL($actor); + if (!empty($profile)) { + Logger::log('Taking key from actor ' . $actor, Logger::DEBUG); + return ['url' => $actor, 'pubkey' => $profile['pubkey']]; + } + } + + return false; + } }