2018-06-18 23:05:44 +02:00
< ? php
/**
2022-01-02 08:27:47 +01:00
* @ copyright Copyright ( C ) 2010 - 2022 , the Friendica project
2020-02-09 16:18:46 +01:00
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
2018-06-18 23:05:44 +02:00
*/
2020-02-09 16:18:46 +01:00
2018-06-18 23:05:44 +02:00
namespace Friendica\Util ;
2018-10-29 22:20:46 +01:00
use Friendica\Core\Logger ;
2020-12-17 19:56:10 +01:00
use Friendica\Database\Database ;
2020-03-04 22:07:05 +01:00
use Friendica\Database\DBA ;
2020-01-19 21:26:42 +01:00
use Friendica\DI ;
2018-09-26 19:24:29 +02:00
use Friendica\Model\APContact ;
2020-12-17 09:00:56 +01:00
use Friendica\Model\Contact ;
2020-03-04 22:07:05 +01:00
use Friendica\Model\User ;
2022-03-28 12:33:06 +02:00
use Friendica\Network\HTTPClient\Client\HttpClient ;
2021-10-23 12:50:31 +02:00
use Friendica\Network\HTTPClient\Client\HttpClientOptions ;
2018-06-18 23:05:44 +02:00
/**
2020-01-19 07:05:23 +01:00
* Implements HTTP Signatures per draft - cavage - http - signatures - 07.
2018-06-18 23:05:44 +02:00
*
2018-06-19 13:30:55 +02:00
* Ported from Hubzilla : https :// framagit . org / hubzilla / core / blob / master / Zotlabs / Web / HTTPSig . php
2018-07-20 04:15:21 +02:00
*
2018-09-23 11:20:25 +02:00
* Other parts of the code for HTTP signing are taken from the Osada project .
* https :// framagit . org / macgirvin / osada
*
2018-06-18 23:05:44 +02:00
* @ see https :// tools . ietf . org / html / draft - cavage - http - signatures - 07
*/
2018-06-20 18:38:23 +02:00
class HTTPSignature
2018-06-18 23:05:44 +02:00
{
// See draft-cavage-http-signatures-08
2018-09-27 00:02:14 +02:00
/**
2020-01-19 07:05:23 +01:00
* Verifies a magic request
2018-09-27 00:02:14 +02:00
*
* @ param $key
*
* @ return array with verification data
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-09-27 00:02:14 +02:00
*/
2018-09-20 20:16:14 +02:00
public static function verifyMagic ( $key )
2018-06-18 23:05:44 +02:00
{
$headers = null ;
$spoofable = false ;
$result = [
'signer' => '' ,
'header_signed' => false ,
2018-09-20 20:16:14 +02:00
'header_valid' => false
2018-06-18 23:05:44 +02:00
];
// Decide if $data arrived via controller submission or curl.
2018-09-20 20:16:14 +02:00
$headers = [];
2022-01-03 19:30:27 +01:00
$headers [ '(request-target)' ] = strtolower ( DI :: args () -> getMethod ()) . ' ' . $_SERVER [ 'REQUEST_URI' ];
2018-06-18 23:05:44 +02:00
2018-09-20 20:16:14 +02:00
foreach ( $_SERVER as $k => $v ) {
if ( strpos ( $k , 'HTTP_' ) === 0 ) {
$field = str_replace ( '_' , '-' , strtolower ( substr ( $k , 5 )));
$headers [ $field ] = $v ;
2018-06-18 23:05:44 +02:00
}
}
$sig_block = null ;
2018-09-20 20:16:14 +02:00
$sig_block = self :: parseSigheader ( $headers [ 'authorization' ]);
2018-06-18 23:05:44 +02:00
if ( ! $sig_block ) {
2021-10-20 20:53:52 +02:00
Logger :: notice ( 'no signature provided.' );
2018-06-18 23:05:44 +02:00
return $result ;
}
$result [ 'header_signed' ] = true ;
$signed_headers = $sig_block [ 'headers' ];
if ( ! $signed_headers ) {
$signed_headers = [ 'date' ];
}
$signed_data = '' ;
foreach ( $signed_headers as $h ) {
if ( array_key_exists ( $h , $headers )) {
$signed_data .= $h . ': ' . $headers [ $h ] . " \n " ;
}
if ( strpos ( $h , '.' )) {
$spoofable = true ;
}
}
$signed_data = rtrim ( $signed_data , " \n " );
2018-09-20 20:16:14 +02:00
$algorithm = 'sha512' ;
2018-06-18 23:05:44 +02:00
2018-06-19 16:15:28 +02:00
if ( $key && function_exists ( $key )) {
2018-06-18 23:05:44 +02:00
$result [ 'signer' ] = $sig_block [ 'keyId' ];
$key = $key ( $sig_block [ 'keyId' ]);
}
2021-10-20 20:53:52 +02:00
Logger :: info ( 'Got keyID ' . $sig_block [ 'keyId' ]);
2018-09-04 19:48:09 +02:00
2018-06-18 23:05:44 +02:00
if ( ! $key ) {
return $result ;
}
$x = Crypto :: rsaVerify ( $signed_data , $sig_block [ 'signature' ], $key , $algorithm );
2021-10-20 20:53:52 +02:00
Logger :: info ( 'verified: ' . $x );
2018-06-18 23:05:44 +02:00
if ( ! $x ) {
return $result ;
}
if ( ! $spoofable ) {
$result [ 'header_valid' ] = true ;
}
return $result ;
}
/**
* @ param array $head
* @ param string $prvkey
* @ param string $keyid ( optional , default 'Key' )
2018-07-20 04:15:21 +02:00
*
2018-06-18 23:05:44 +02:00
* @ return array
*/
2018-09-20 20:16:14 +02:00
public static function createSig ( $head , $prvkey , $keyid = 'Key' )
2018-06-18 23:05:44 +02:00
{
$return_headers = [];
2021-08-25 13:45:00 +02:00
if ( ! empty ( $head )) {
$return_headers = $head ;
}
2018-06-18 23:05:44 +02:00
2018-09-20 20:16:14 +02:00
$alg = 'sha512' ;
$algorithm = 'rsa-sha512' ;
2018-06-18 23:05:44 +02:00
2018-09-20 20:16:14 +02:00
$x = self :: sign ( $head , $prvkey , $alg );
2018-06-18 23:05:44 +02:00
$headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm
. '",headers="' . $x [ 'headers' ] . '",signature="' . $x [ 'signature' ] . '"' ;
2021-08-25 13:45:00 +02:00
$return_headers [ 'Authorization' ] = [ 'Signature ' . $headerval ];
2018-06-18 23:05:44 +02:00
return $return_headers ;
}
/**
* @ param array $head
* @ param string $prvkey
* @ param string $alg ( optional ) default 'sha256'
2018-07-20 04:15:21 +02:00
*
2018-06-18 23:05:44 +02:00
* @ return array
*/
2018-09-20 20:16:14 +02:00
private static function sign ( $head , $prvkey , $alg = 'sha256' )
2018-06-18 23:05:44 +02:00
{
$ret = [];
$headers = '' ;
$fields = '' ;
2018-09-20 20:16:14 +02:00
foreach ( $head as $k => $v ) {
2021-08-25 13:45:00 +02:00
if ( is_array ( $v )) {
$v = implode ( ', ' , $v );
}
2018-09-20 20:16:14 +02:00
$headers .= strtolower ( $k ) . ': ' . trim ( $v ) . " \n " ;
if ( $fields ) {
$fields .= ' ' ;
2018-06-18 23:05:44 +02:00
}
2018-09-20 20:16:14 +02:00
$fields .= strtolower ( $k );
2018-06-18 23:05:44 +02:00
}
2018-09-20 20:16:14 +02:00
// strip the trailing linefeed
$headers = rtrim ( $headers , " \n " );
2018-06-18 23:05:44 +02:00
$sig = base64_encode ( Crypto :: rsaSign ( $headers , $prvkey , $alg ));
$ret [ 'headers' ] = $fields ;
$ret [ 'signature' ] = $sig ;
2018-07-20 04:15:21 +02:00
2018-06-18 23:05:44 +02:00
return $ret ;
}
/**
* @ param string $header
2020-07-14 15:48:04 +02:00
* @ return array associative array with
2018-06-18 23:05:44 +02:00
* - \e string \b keyID
2020-07-14 15:48:04 +02:00
* - \e string \b created
* - \e string \b expires
2018-06-18 23:05:44 +02:00
* - \e string \b algorithm
* - \e array \b headers
* - \e string \b signature
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-06-18 23:05:44 +02:00
*/
public static function parseSigheader ( $header )
{
2020-07-14 15:48:04 +02:00
// Remove obsolete folds
$header = preg_replace ( '/\n\s+/' , ' ' , $header );
2018-06-18 23:05:44 +02:00
2020-07-14 15:48:04 +02:00
$token = " [!# $ %&'*+.^_`|~0-9A-Za-z-] " ;
2018-06-18 23:05:44 +02:00
2020-07-14 15:48:04 +02:00
$quotedString = '"(?:\\\\.|[^"\\\\])*"' ;
2018-06-18 23:05:44 +02:00
2020-07-14 15:48:04 +02:00
$regex = " /( $token +)=( $quotedString | $token +)/ism " ;
$matches = [];
preg_match_all ( $regex , $header , $matches , PREG_SET_ORDER );
2018-06-18 23:05:44 +02:00
2020-07-14 15:48:04 +02:00
$headers = [];
foreach ( $matches as $match ) {
$headers [ $match [ 1 ]] = trim ( $match [ 2 ] ? : $match [ 3 ], '"' );
2018-06-18 23:05:44 +02:00
}
2020-07-14 15:48:04 +02:00
// if the header is encrypted, decrypt with (default) site private key and continue
if ( ! empty ( $headers [ 'iv' ])) {
$header = self :: decryptSigheader ( $headers , DI :: config () -> get ( 'system' , 'prvkey' ));
return self :: parseSigheader ( $header );
2018-06-18 23:05:44 +02:00
}
2020-07-14 15:48:04 +02:00
$return = [
'keyId' => $headers [ 'keyId' ] ? ? '' ,
'algorithm' => $headers [ 'algorithm' ] ? ? 'rsa-sha256' ,
'created' => $headers [ 'created' ] ? ? null ,
'expires' => $headers [ 'expires' ] ? ? null ,
'headers' => explode ( ' ' , $headers [ 'headers' ] ? ? '' ),
'signature' => base64_decode ( preg_replace ( '/\s+/' , '' , $headers [ 'signature' ] ? ? '' )),
];
if ( ! empty ( $return [ 'signature' ]) && ! empty ( $return [ 'algorithm' ]) && empty ( $return [ 'headers' ])) {
$return [ 'headers' ] = [ 'date' ];
2018-06-18 23:05:44 +02:00
}
2020-07-14 15:48:04 +02:00
return $return ;
2018-06-18 23:05:44 +02:00
}
/**
2020-07-14 15:48:04 +02:00
* @ param array $headers Signature headers
* @ param string $prvkey The site private key
* @ return string Decrypted signature string
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-06-18 23:05:44 +02:00
*/
2020-07-14 15:48:04 +02:00
private static function decryptSigheader ( array $headers , string $prvkey )
2018-06-18 23:05:44 +02:00
{
2020-07-14 15:48:04 +02:00
if ( ! empty ( $headers [ 'iv' ]) && ! empty ( $headers [ 'key' ]) && ! empty ( $headers [ 'data' ])) {
return Crypto :: unencapsulate ( $headers , $prvkey );
2018-06-18 23:05:44 +02:00
}
return '' ;
}
2018-09-20 20:16:14 +02:00
2018-09-27 14:01:16 +02:00
/*
2018-09-20 20:16:14 +02:00
* Functions for ActivityPub
*/
2018-09-27 00:02:14 +02:00
/**
2020-01-19 07:05:23 +01:00
* Transmit given data to a target for a user
2018-09-27 00:02:14 +02:00
*
2019-01-06 22:06:53 +01:00
* @ 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
2018-10-23 05:54:18 +02:00
*
* @ return boolean Was the transmission successful ?
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-09-27 00:02:14 +02:00
*/
2018-09-20 20:16:14 +02:00
public static function transmit ( $data , $target , $uid )
{
$owner = User :: getOwnerDataById ( $uid );
if ( ! $owner ) {
return ;
}
2018-09-22 00:31:33 +02:00
$content = json_encode ( $data , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
2018-09-20 20:16:14 +02:00
// 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 );
2019-01-14 13:10:11 +01:00
$date = DateTimeFormat :: utcNow ( DateTimeFormat :: HTTP );
2018-09-20 20:16:14 +02:00
2021-08-24 14:17:42 +02:00
$headers = [
'Date' => $date ,
'Content-Length' => $content_length ,
'Digest' => $digest ,
'Host' => $host
];
2018-09-20 20:16:14 +02:00
2019-01-14 13:10:11 +01:00
$signed_data = " (request-target): post " . $path . " \n date: " . $date . " \n content-length: " . $content_length . " \n digest: " . $digest . " \n host: " . $host ;
2018-09-20 20:16:14 +02:00
$signature = base64_encode ( Crypto :: rsaSign ( $signed_data , $owner [ 'uprvkey' ], 'sha256' ));
2021-08-24 14:17:42 +02:00
$headers [ 'Signature' ] = 'keyId="' . $owner [ 'url' ] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date content-length digest host",signature="' . $signature . '"' ;
2018-09-20 20:16:14 +02:00
2021-08-24 14:17:42 +02:00
$headers [ 'Content-Type' ] = 'application/activity+json' ;
2018-09-20 20:16:14 +02:00
2021-08-25 21:54:54 +02:00
$postResult = DI :: httpClient () -> post ( $target , $content , $headers );
2018-10-23 05:54:18 +02:00
$return_code = $postResult -> getReturnCode ();
2021-10-20 20:53:52 +02:00
Logger :: info ( 'Transmit to ' . $target . ' returned ' . $return_code );
2018-09-20 20:16:14 +02:00
2019-03-25 22:51:32 +01:00
$success = ( $return_code >= 200 ) && ( $return_code <= 299 );
self :: setInboxStatus ( $target , $success );
return $success ;
}
/**
2020-01-19 07:05:23 +01:00
* Set the delivery status for a given inbox
2019-03-25 22:51:32 +01:00
*
* @ param string $url The URL of the inbox
* @ param boolean $success Transmission status
2020-11-23 20:25:22 +01:00
* @ param boolean $shared The inbox is a shared inbox
2019-03-25 22:51:32 +01:00
*/
2020-11-23 20:25:22 +01:00
static public function setInboxStatus ( $url , $success , $shared = false )
2019-03-25 22:51:32 +01:00
{
$now = DateTimeFormat :: utcNow ();
$status = DBA :: selectFirst ( 'inbox-status' , [], [ 'url' => $url ]);
if ( ! DBA :: isResult ( $status )) {
2020-12-17 19:56:10 +01:00
DBA :: insert ( 'inbox-status' , [ 'url' => $url , 'created' => $now , 'shared' => $shared ], Database :: INSERT_IGNORE );
2019-03-25 22:51:32 +01:00
$status = DBA :: selectFirst ( 'inbox-status' , [], [ 'url' => $url ]);
}
if ( $success ) {
$fields = [ 'success' => $now ];
} else {
$fields = [ 'failure' => $now ];
}
if ( $status [ 'failure' ] > DBA :: NULL_DATETIME ) {
$new_previous_stamp = strtotime ( $status [ 'failure' ]);
$old_previous_stamp = strtotime ( $status [ 'previous' ]);
// Only set "previous" with at least one day difference.
// We use this to assure to not accidentally archive too soon.
if (( $new_previous_stamp - $old_previous_stamp ) >= 86400 ) {
$fields [ 'previous' ] = $status [ 'failure' ];
}
}
if ( ! $success ) {
if ( $status [ 'success' ] <= DBA :: NULL_DATETIME ) {
$stamp1 = strtotime ( $status [ 'created' ]);
} else {
$stamp1 = strtotime ( $status [ 'success' ]);
}
$stamp2 = strtotime ( $now );
$previous_stamp = strtotime ( $status [ 'previous' ]);
// Archive the inbox when there had been failures for five days.
// Additionally ensure that at least one previous attempt has to be in between.
if ((( $stamp2 - $stamp1 ) >= 86400 * 5 ) && ( $previous_stamp > $stamp1 )) {
$fields [ 'archive' ] = true ;
}
} else {
$fields [ 'archive' ] = false ;
}
DBA :: update ( 'inbox-status' , $fields , [ 'url' => $url ]);
2018-09-20 20:16:14 +02:00
}
2018-11-03 22:37:08 +01:00
/**
2020-01-19 07:05:23 +01:00
* Fetches JSON data for a user
2018-11-03 22:37:08 +01:00
*
2019-01-06 22:06:53 +01:00
* @ param string $request request url
* @ param integer $uid User id of the requester
2018-11-03 22:37:08 +01:00
*
* @ return array JSON array
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-11-03 22:37:08 +01:00
*/
public static function fetch ( $request , $uid )
{
2020-12-15 23:56:46 +01:00
$curlResult = self :: fetchRaw ( $request , $uid );
2018-11-03 22:37:08 +01:00
2019-03-18 23:33:20 +01:00
if ( empty ( $curlResult )) {
return false ;
2018-11-03 22:37:08 +01:00
}
2019-03-18 23:33:20 +01:00
if ( ! $curlResult -> isSuccess () || empty ( $curlResult -> getBody ())) {
return false ;
}
2018-11-03 22:37:08 +01:00
2019-03-18 23:33:20 +01:00
$content = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $content ) || ! is_array ( $content )) {
return false ;
}
2018-11-03 22:37:08 +01:00
2019-03-18 23:33:20 +01:00
return $content ;
}
2018-11-03 22:37:08 +01:00
2019-03-18 23:33:20 +01:00
/**
2020-01-19 07:05:23 +01:00
* Fetches raw data for a user
2019-03-18 23:33:20 +01:00
*
* @ param string $request request url
* @ param integer $uid User id of the requester
* @ param boolean $binary TRUE if asked to return binary results ( file download ) ( default is " false " )
* @ param array $opts ( optional parameters ) assoziative array with :
* 'accept_content' => supply Accept : header with 'accept_content' as the value
* 'timeout' => int Timeout in seconds , default system config value or 60 seconds
* 'nobody' => only return the header
* 'cookiejar' => path to cookie jar file
*
2021-10-23 12:50:31 +02:00
* @ return \Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses CurlResult
2019-03-18 23:33:20 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2022-03-29 10:57:38 +02:00
public static function fetchRaw ( $request , $uid = 0 , $opts = [ HttpClientOptions :: ACCEPT_CONTENT => [ HttpClient :: ACCEPT_JSON_AS ]])
2019-03-18 23:33:20 +01:00
{
2020-10-18 22:31:26 +02:00
$header = [];
2020-08-07 19:05:49 +02:00
2019-03-18 23:33:20 +01:00
if ( ! empty ( $uid )) {
$owner = User :: getOwnerDataById ( $uid );
if ( ! $owner ) {
return ;
}
2020-08-22 16:48:09 +02:00
} else {
$owner = User :: getSystemAccount ();
if ( ! $owner ) {
return ;
}
}
2018-11-03 22:37:08 +01:00
2020-08-22 16:48:09 +02:00
if ( ! empty ( $owner [ 'uprvkey' ])) {
// Header data that is about to be signed.
$host = parse_url ( $request , PHP_URL_HOST );
$path = parse_url ( $request , PHP_URL_PATH );
$date = DateTimeFormat :: utcNow ( DateTimeFormat :: HTTP );
2018-11-03 22:37:08 +01:00
2021-08-24 14:17:42 +02:00
$header [ 'Date' ] = $date ;
$header [ 'Host' ] = $host ;
2018-11-03 22:37:08 +01:00
2020-08-22 16:48:09 +02:00
$signed_data = " (request-target): get " . $path . " \n date: " . $date . " \n host: " . $host ;
2018-11-03 22:37:08 +01:00
2020-08-22 16:48:09 +02:00
$signature = base64_encode ( Crypto :: rsaSign ( $signed_data , $owner [ 'uprvkey' ], 'sha256' ));
2018-11-03 22:37:08 +01:00
2021-08-24 14:17:42 +02:00
$header [ 'Signature' ] = 'keyId="' . $owner [ 'url' ] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) date host",signature="' . $signature . '"' ;
2018-11-03 22:37:08 +01:00
}
2021-08-25 21:45:15 +02:00
$curl_opts = $opts ;
2021-10-23 12:50:31 +02:00
$curl_opts [ HttpClientOptions :: HEADERS ] = $header ;
2019-03-18 23:33:20 +01:00
2020-10-20 17:19:06 +02:00
if ( ! empty ( $opts [ 'nobody' ])) {
2021-08-25 21:54:54 +02:00
$curlResult = DI :: httpClient () -> head ( $request , $curl_opts );
2020-10-18 22:15:20 +02:00
} else {
2021-08-25 21:54:54 +02:00
$curlResult = DI :: httpClient () -> get ( $request , $curl_opts );
2020-10-18 22:15:20 +02:00
}
2019-03-18 23:33:20 +01:00
$return_code = $curlResult -> getReturnCode ();
2021-10-20 20:53:52 +02:00
Logger :: info ( 'Fetched for user ' . $uid . ' from ' . $request . ' returned ' . $return_code );
2019-03-18 23:33:20 +01:00
return $curlResult ;
2018-11-03 22:37:08 +01:00
}
2018-09-27 00:02:14 +02:00
/**
2020-01-19 07:05:23 +01:00
* Gets a signer from a given HTTP request
2018-09-27 00:02:14 +02:00
*
* @ param $content
* @ param $http_headers
*
2019-01-06 22:06:53 +01:00
* @ return string Signer
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-09-27 00:02:14 +02:00
*/
2018-09-22 00:31:33 +02:00
public static function getSigner ( $content , $http_headers )
2018-09-20 20:16:14 +02:00
{
2019-01-15 07:31:12 +01:00
if ( empty ( $http_headers [ 'HTTP_SIGNATURE' ])) {
2021-10-30 01:21:07 +02:00
Logger :: debug ( 'No HTTP_SIGNATURE header' );
2018-09-20 20:16:14 +02:00
return false ;
}
2019-01-15 07:31:12 +01:00
if ( ! empty ( $content )) {
$object = json_decode ( $content , true );
if ( empty ( $object )) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'No object' );
2019-01-15 07:31:12 +01:00
return false ;
}
$actor = JsonLD :: fetchElement ( $object , 'actor' , 'id' );
} else {
$actor = '' ;
}
2018-09-20 20:16:14 +02:00
$headers = [];
2022-01-03 19:30:27 +01:00
$headers [ '(request-target)' ] = strtolower ( DI :: args () -> getMethod ()) . ' ' . parse_url ( $http_headers [ 'REQUEST_URI' ], PHP_URL_PATH );
2018-09-20 20:16:14 +02:00
// 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' ]);
2022-01-21 16:38:33 +01:00
// Add fields from the signature block to the header. See issue 8845
if ( ! empty ( $sig_block [ 'created' ]) && empty ( $headers [ '(created)' ])) {
$headers [ '(created)' ] = $sig_block [ 'created' ];
}
if ( ! empty ( $sig_block [ 'expires' ]) && empty ( $headers [ '(expires)' ])) {
$headers [ '(expires)' ] = $sig_block [ 'expires' ];
}
2018-09-20 20:16:14 +02:00
if ( empty ( $sig_block ) || empty ( $sig_block [ 'headers' ]) || empty ( $sig_block [ 'keyId' ])) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'No headers or keyId' );
2018-09-20 20:16:14 +02:00
return false ;
}
$signed_data = '' ;
foreach ( $sig_block [ 'headers' ] as $h ) {
if ( array_key_exists ( $h , $headers )) {
$signed_data .= $h . ': ' . $headers [ $h ] . " \n " ;
2022-01-21 16:38:33 +01:00
} else {
Logger :: info ( 'Requested header field not found' , [ 'field' => $h , 'header' => $headers ]);
2018-09-20 20:16:14 +02:00
}
}
$signed_data = rtrim ( $signed_data , " \n " );
if ( empty ( $signed_data )) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'Signed data is empty' );
2018-09-20 20:16:14 +02:00
return false ;
}
$algorithm = null ;
2020-07-04 19:12:59 +02:00
// Wildcard value where signing algorithm should be derived from keyId
// @see https://tools.ietf.org/html/draft-ietf-httpbis-message-signatures-00#section-4.1
// Defaulting to SHA256 as it seems to be the prevalent implementation
// @see https://arewehs2019yet.vpzom.click
if ( $sig_block [ 'algorithm' ] === 'hs2019' ) {
$algorithm = 'sha256' ;
}
2018-09-20 20:16:14 +02:00
if ( $sig_block [ 'algorithm' ] === 'rsa-sha256' ) {
$algorithm = 'sha256' ;
}
if ( $sig_block [ 'algorithm' ] === 'rsa-sha512' ) {
$algorithm = 'sha512' ;
}
if ( empty ( $algorithm )) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'No alagorithm' );
2018-09-20 20:16:14 +02:00
return false ;
}
$key = self :: fetchKey ( $sig_block [ 'keyId' ], $actor );
2020-12-17 09:00:56 +01:00
if ( empty ( $key )) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'Empty key' );
2020-12-17 09:00:56 +01:00
return false ;
}
if ( ! empty ( $key [ 'url' ]) && ! empty ( $key [ 'type' ]) && ( $key [ 'type' ] == 'Tombstone' )) {
Logger :: info ( 'Actor is a tombstone' , [ 'key' => $key ]);
2021-05-22 10:25:30 +02:00
if ( ! Contact :: isLocal ( $key [ 'url' ])) {
// We now delete everything that we possibly knew from this actor
Contact :: deleteContactByUrl ( $key [ 'url' ]);
}
2021-05-01 14:32:33 +02:00
return null ;
2020-12-17 09:00:56 +01:00
}
2018-09-20 20:16:14 +02:00
2020-12-17 09:00:56 +01:00
if ( empty ( $key [ 'pubkey' ])) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'Empty pubkey' );
2018-09-20 20:16:14 +02:00
return false ;
}
2018-09-22 00:31:33 +02:00
if ( ! Crypto :: rsaVerify ( $signed_data , $sig_block [ 'signature' ], $key [ 'pubkey' ], $algorithm )) {
2022-01-21 16:38:33 +01:00
Logger :: info ( 'Verification failed' , [ 'signed_data' => $signed_data , 'algorithm' => $algorithm , 'header' => $sig_block [ 'headers' ], 'http_headers' => $http_headers ]);
2018-09-20 20:16:14 +02:00
return false ;
}
2019-03-19 07:44:51 +01:00
$hasGoodSignedContent = false ;
2018-09-20 20:16:14 +02:00
// Check the digest when it is part of the signed data
2019-03-19 07:44:51 +01:00
if ( ! empty ( $content ) && in_array ( 'digest' , $sig_block [ 'headers' ])) {
2018-09-20 20:16:14 +02:00
$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 ]) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'Digest does not match' );
2018-09-20 20:16:14 +02:00
return false ;
}
2019-03-19 07:44:51 +01:00
$hasGoodSignedContent = true ;
2018-09-20 20:16:14 +02:00
}
2022-01-21 16:38:33 +01:00
if ( in_array ( 'date' , $sig_block [ 'headers' ]) && ! empty ( $headers [ 'date' ])) {
$created = strtotime ( $headers [ 'date' ]);
} elseif ( in_array ( '(created)' , $sig_block [ 'headers' ]) && ! empty ( $sig_block [ 'created' ])) {
$created = $sig_block [ 'created' ];
} else {
$created = 0 ;
}
if ( in_array ( '(expires)' , $sig_block [ 'headers' ]) && ! empty ( $sig_block [ 'expires' ])) {
$expired = min ( $sig_block [ 'expires' ], $created + 300 );
} else {
$expired = $created + 300 ;
}
2019-01-14 17:03:13 +01:00
// Check if the signed date field is in an acceptable range
2022-01-21 16:38:33 +01:00
if ( ! empty ( $created )) {
$current = time ();
2022-02-12 18:27:58 +01:00
// Calculate with a grace period of 60 seconds to avoid slight time differences between the servers
if (( $created - 60 ) > $current ) {
2022-01-21 16:38:33 +01:00
Logger :: notice ( 'Signature created in the future' , [ 'created' => date ( DateTimeFormat :: MYSQL , $created ), 'expired' => date ( DateTimeFormat :: MYSQL , $expired ), 'current' => date ( DateTimeFormat :: MYSQL , $current )]);
2019-01-14 17:03:13 +01:00
return false ;
}
2022-01-21 16:38:33 +01:00
if ( $current > $expired ) {
Logger :: notice ( 'Signature expired' , [ 'created' => date ( DateTimeFormat :: MYSQL , $created ), 'expired' => date ( DateTimeFormat :: MYSQL , $expired ), 'current' => date ( DateTimeFormat :: MYSQL , $current )]);
return false ;
}
Logger :: debug ( 'Valid creation date' , [ 'created' => date ( DateTimeFormat :: MYSQL , $created ), 'expired' => date ( DateTimeFormat :: MYSQL , $expired ), 'current' => date ( DateTimeFormat :: MYSQL , $current )]);
2019-03-19 07:44:51 +01:00
$hasGoodSignedContent = true ;
2019-01-14 17:03:13 +01:00
}
2018-10-24 21:19:51 +02:00
2018-09-20 20:16:14 +02:00
// 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' ]) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'Content length does not match' );
2018-09-20 20:16:14 +02:00
return false ;
}
}
2019-03-19 07:44:51 +01:00
// Ensure that the authentication had been done with some content
// Without this check someone could authenticate with fakeable data
if ( ! $hasGoodSignedContent ) {
2021-05-01 14:32:33 +02:00
Logger :: info ( 'No good signed content' );
2019-03-19 07:44:51 +01:00
return false ;
}
2018-09-22 00:31:33 +02:00
return $key [ 'url' ];
2018-09-20 20:16:14 +02:00
}
2018-09-27 00:02:14 +02:00
/**
2020-01-19 07:05:23 +01:00
* fetches a key for a given id and actor
2018-09-27 00:02:14 +02:00
*
* @ param $id
* @ param $actor
*
* @ return array with actor url and public key
2019-01-06 22:06:53 +01:00
* @ throws \Exception
2018-09-27 00:02:14 +02:00
*/
2018-09-20 20:16:14 +02:00
private static function fetchKey ( $id , $actor )
{
$url = ( strpos ( $id , '#' ) ? substr ( $id , 0 , strpos ( $id , '#' )) : $id );
2018-09-30 10:14:05 +02:00
$profile = APContact :: getByURL ( $url );
2018-09-20 20:16:14 +02:00
if ( ! empty ( $profile )) {
2021-05-25 15:18:48 +02:00
Logger :: info ( 'Taking key from id' , [ 'id' => $id ]);
2020-12-17 09:00:56 +01:00
return [ 'url' => $url , 'pubkey' => $profile [ 'pubkey' ], 'type' => $profile [ 'type' ]];
2018-09-20 20:16:14 +02:00
} elseif ( $url != $actor ) {
2018-09-30 10:14:05 +02:00
$profile = APContact :: getByURL ( $actor );
2018-09-20 20:16:14 +02:00
if ( ! empty ( $profile )) {
2021-05-25 15:18:48 +02:00
Logger :: info ( 'Taking key from actor' , [ 'actor' => $actor ]);
2020-12-17 09:00:56 +01:00
return [ 'url' => $actor , 'pubkey' => $profile [ 'pubkey' ], 'type' => $profile [ 'type' ]];
2018-09-20 20:16:14 +02:00
}
}
2021-05-25 15:18:48 +02:00
Logger :: notice ( 'Key could not be fetched' , [ 'url' => $url , 'actor' => $actor ]);
2018-09-20 20:16:14 +02:00
return false ;
}
2018-06-18 23:05:44 +02:00
}