mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-26 09:53:20 +01:00
fixed many issues with dkim signing
This commit is contained in:
parent
81376af3f3
commit
46acebf2a7
@ -23,27 +23,49 @@ class ischedule_client
|
|||||||
*/
|
*/
|
||||||
const VERSION = '1.0';
|
const VERSION = '1.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required headers in DKIM signature (DKIM-Signature is always a required header!)
|
||||||
|
*/
|
||||||
|
const REQUIRED_DKIM_HEADERS = 'Host:iSchedule-Version:iSchedule-Message-ID:Content-Type:Originator:Recipient';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL to use to contact iSchedule receiver
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
private $url;
|
private $url;
|
||||||
|
|
||||||
private $recipient;
|
/**
|
||||||
|
* Recipient email addresses
|
||||||
|
*
|
||||||
|
* @param array
|
||||||
|
*/
|
||||||
|
private $recipients;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Originator email address
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
private $originator;
|
private $originator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private key of originators domain
|
* Private key of originators domain
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
*/
|
*/
|
||||||
private $dkim_private_key;
|
private $dkim_private_key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param string $recipient=null recipient email-address
|
* @param string|array $recipients=null recipient email-address(es)
|
||||||
* @param string $url=null ischedule url, if it should NOT be discovered
|
* @param string $url=null ischedule url, if it should NOT be discovered
|
||||||
* @throws Exception in case of an error or discovery failure
|
* @throws Exception in case of an error or discovery failure
|
||||||
*/
|
*/
|
||||||
public function __construct($recipient, $url=null)
|
public function __construct($recipients, $url=null)
|
||||||
{
|
{
|
||||||
$this->recipient = $recipient;
|
$this->recipients = (array)$recipients;
|
||||||
$this->originator = $GLOBALS['egw_info']['user']['account_email'];
|
$this->originator = $GLOBALS['egw_info']['user']['account_email'];
|
||||||
|
|
||||||
if (is_null($url))
|
if (is_null($url))
|
||||||
@ -177,13 +199,17 @@ class ischedule_client
|
|||||||
'iSchedule-Message-ID' => uniqid(),
|
'iSchedule-Message-ID' => uniqid(),
|
||||||
'Content-Type' => $content_type,
|
'Content-Type' => $content_type,
|
||||||
'Originator' => $this->originator,
|
'Originator' => $this->originator,
|
||||||
'Recipient' => $this->recipient,
|
'Recipient' => $this->recipients,
|
||||||
|
'Cache-Control' => 'no-cache, no-transform', // required by iSchedule spec
|
||||||
'Content-Length' => bytes($content),
|
'Content-Length' => bytes($content),
|
||||||
);
|
);
|
||||||
$header_string = '';
|
$header_string = '';
|
||||||
foreach($headers as $name => $value)
|
foreach($headers as $name => $value)
|
||||||
{
|
{
|
||||||
$header_string .= $name.': '.$value."\r\n";
|
foreach((array)$value as $val)
|
||||||
|
{
|
||||||
|
$header_string .= $name.': '.$val."\r\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$header_string .= $this->dkim_sign($headers, $content)."\r\n";
|
$header_string .= $this->dkim_sign($headers, $content)."\r\n";
|
||||||
|
|
||||||
@ -192,7 +218,7 @@ class ischedule_client
|
|||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'header' => $header_string,
|
'header' => $header_string,
|
||||||
'user_agent' => 'EGroupware iSchedule client '.$GLOBALS['egw_info']['server']['versions']['phpgwapi'].' $Id$',
|
'user_agent' => 'EGroupware iSchedule client '.$GLOBALS['egw_info']['server']['versions']['phpgwapi'].' $Id$',
|
||||||
//'follow_location' => 1, // default 1=follow, but only for POST!
|
//'follow_location' => 1, // default 1=follow, but only for GET, not POST!
|
||||||
//'timeout' => $timeout, // max timeout in seconds (float)
|
//'timeout' => $timeout, // max timeout in seconds (float)
|
||||||
'content' => $content,
|
'content' => $content,
|
||||||
)
|
)
|
||||||
@ -233,21 +259,30 @@ class ischedule_client
|
|||||||
* @param array $headers name => value pairs, names as in $sign_headers
|
* @param array $headers name => value pairs, names as in $sign_headers
|
||||||
* @param string $body
|
* @param string $body
|
||||||
* @param string $selector='calendar'
|
* @param string $selector='calendar'
|
||||||
* @param string $sign_headers='Content-Type:Host:Originator:Recipient'
|
* @param string $sign_headers='iSchedule-Version:Content-Type:Originator:Recipient'
|
||||||
* @return string DKIM-Signature: ...
|
* @return string DKIM-Signature: ...
|
||||||
*/
|
*/
|
||||||
public function dkim_sign(array $headers, $body, $selector='calendar')
|
public function dkim_sign(array $headers, $body, $selector='calendar',$sign_headers=self::REQUIRED_DKIM_HEADERS)
|
||||||
{
|
{
|
||||||
$dkim_headers = array();
|
$header_values = $header_names = array();
|
||||||
foreach($headers as $header => $value)
|
foreach(explode(':', $sign_headers) as $header)
|
||||||
{
|
{
|
||||||
$dkim_headers[] = $header.': '.$value;
|
foreach((array)$headers[$header] as $value)
|
||||||
|
{
|
||||||
|
$header_values[] = $header.': '.$value;
|
||||||
|
$header_names[] = $header;
|
||||||
|
}
|
||||||
|
// oversign multiple value header Recipient
|
||||||
|
if ($header == 'Recipient')
|
||||||
|
{
|
||||||
|
$header_names[] = $header;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
include_once EGW_API_INC.'/php-mail-domain-signer/lib/class.mailDomainSigner.php';
|
include_once EGW_API_INC.'/php-mail-domain-signer/lib/class.mailDomainSigner.php';
|
||||||
list(,$domain) = explode('@', $this->originator);
|
list(,$domain) = explode('@', $this->originator);
|
||||||
$mds = new mailDomainSigner($this->dkim_private_key, $domain, $selector);
|
$mds = new mailDomainSigner($this->dkim_private_key, $domain, $selector);
|
||||||
// generate DKIM signature according to iSchedule spec
|
// generate DKIM signature according to iSchedule spec
|
||||||
$dkim = $mds->getDKIM(implode(':', array_keys($headers)), $dkim_headers, $body, 'relaxed/simple', 'rsa/sha256',
|
$dkim = $mds->getDKIM(implode(':', $header_names), $header_values, $body, 'relaxed/simple', 'rsa-sha256',
|
||||||
"DKIM-Signature: ".
|
"DKIM-Signature: ".
|
||||||
"v=1; ". // DKIM Version
|
"v=1; ". // DKIM Version
|
||||||
"a=\$a; ". // The algorithm used to generate the signature "rsa-sha1"
|
"a=\$a; ". // The algorithm used to generate the signature "rsa-sha1"
|
||||||
|
@ -33,7 +33,8 @@ class ischedule_server extends groupdav
|
|||||||
/**
|
/**
|
||||||
* Required headers in DKIM signature (DKIM-Signature is always a required header!)
|
* Required headers in DKIM signature (DKIM-Signature is always a required header!)
|
||||||
*/
|
*/
|
||||||
const REQUIRED_DKIM_HEADERS = 'iSchedule-Version:iSchedule-Message-ID:Content-Type:Originator:Recipient';
|
const REQUIRED_DKIM_HEADERS = 'iSchedule-Version:Content-Type:Originator:Recipient';
|
||||||
|
//const REQUIRED_DKIM_HEADERS = 'iSchedule-Version:iSchedule-Message-ID:Content-Type:Originator:Recipient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -130,8 +131,7 @@ class ischedule_server extends groupdav
|
|||||||
// for multivalued Recipient header: as PHP engine agregates them ", " separated,
|
// for multivalued Recipient header: as PHP engine agregates them ", " separated,
|
||||||
// we cant tell it apart from ", " separated recipients in one header, therefore we try to validate both.
|
// we cant tell it apart from ", " separated recipients in one header, therefore we try to validate both.
|
||||||
// It will fail if multiple recipients in a single header are also ", " separated (just comma works fine)
|
// It will fail if multiple recipients in a single header are also ", " separated (just comma works fine)
|
||||||
if (!self::dkim_validate($headers, $this->request, $error, true) &&
|
if (!self::dkim_validate($headers, $this->request, $error))
|
||||||
!self::dkim_validate($headers, $this->request, $error, false))
|
|
||||||
{
|
{
|
||||||
throw new Exception('Bad Request: DKIM signature invalid: '.$error, 400);
|
throw new Exception('Bad Request: DKIM signature invalid: '.$error, 400);
|
||||||
}
|
}
|
||||||
@ -315,6 +315,14 @@ class ischedule_server extends groupdav
|
|||||||
$xml->endElement(); // response
|
$xml->endElement(); // response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Required DKIM tags
|
||||||
|
*
|
||||||
|
* @link https://tools.ietf.org/html/rfc6376#section-3.5
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $required_dkim_tags = array('v', 'a', 'b', 'bh', 'd', 'h', 's');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate DKIM signature
|
* Validate DKIM signature
|
||||||
@ -344,6 +352,20 @@ class ischedule_server extends groupdav
|
|||||||
}
|
}
|
||||||
$dkim = array_combine($matches[1], $matches[2]);
|
$dkim = array_combine($matches[1], $matches[2]);
|
||||||
|
|
||||||
|
// check required DKIM tags
|
||||||
|
if (($missing = array_diff(self::$required_dkim_tags, array_keys($dkim))))
|
||||||
|
{
|
||||||
|
$error = 'missing required DKIM tags: '.implode(', ', $missing);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check dkim version, have to fail if it's not 1
|
||||||
|
if ($dkim['v'] !== '1')
|
||||||
|
{
|
||||||
|
$error = "Wrong DKIM version '$dkim[v]'";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// create headers array
|
// create headers array
|
||||||
$dkim_headers = array();
|
$dkim_headers = array();
|
||||||
$check = $headers;
|
$check = $headers;
|
||||||
@ -365,10 +387,11 @@ class ischedule_server extends groupdav
|
|||||||
}
|
}
|
||||||
$dkim_headers[] = $header.': '.$value;
|
$dkim_headers[] = $header.': '.$value;
|
||||||
}
|
}
|
||||||
list($dkim_unsigned) = explode('b='.$dkim['b'], 'DKIM-Signature: '.$headers['dkim-signature']);
|
// dkim signature is obvious without content of signature, but must not necessarly be last tag
|
||||||
$dkim_unsigned .= 'b=';
|
$dkim_unsigned = 'DKIM-Signature: '.str_replace($dkim['b'], '', $headers['dkim-signature']);
|
||||||
|
|
||||||
list($header_canon, $body_canon) = explode('/', $dkim['c']);
|
// c defaults to 'simple/simple', check on valid canonicalization methods is performed further down
|
||||||
|
list($header_canon, $body_canon) = explode('/', isset($dkim['c']) ? $dkim['c'] : 'simple/simple');
|
||||||
require_once EGW_API_INC.'/php-mail-domain-signer/lib/class.mailDomainSigner.php';
|
require_once EGW_API_INC.'/php-mail-domain-signer/lib/class.mailDomainSigner.php';
|
||||||
|
|
||||||
// Canonicalization for Body
|
// Canonicalization for Body
|
||||||
@ -387,8 +410,15 @@ class ischedule_server extends groupdav
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check signing and hashing algorithms
|
||||||
|
list($sign_algo, $hash_algo) = explode('-', $dkim['a']);
|
||||||
|
if ($sign_algo != 'rsa' || !in_array($hash_algo, array('sha1', 'sha256', 'sha384', 'sha512')))
|
||||||
|
{
|
||||||
|
$error = "Unknown or unimplemented algorithm a='$dkim[a]'";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Hash of the canonicalized body [tag:bh]
|
// Hash of the canonicalized body [tag:bh]
|
||||||
list(,$hash_algo) = explode('-', $dkim['a']);
|
|
||||||
$_bh = base64_encode(hash($hash_algo, $_b, true));
|
$_bh = base64_encode(hash($hash_algo, $_b, true));
|
||||||
|
|
||||||
// check body hash
|
// check body hash
|
||||||
@ -410,9 +440,6 @@ class ischedule_server extends groupdav
|
|||||||
$_unsigned = mailDomainSigner::headSimpleCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned);
|
$_unsigned = mailDomainSigner::headSimpleCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'http/well-known':
|
|
||||||
// todo
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
$error = "Unknown header canonicalization '$header_canon'";
|
$error = "Unknown header canonicalization '$header_canon'";
|
||||||
return false;
|
return false;
|
||||||
@ -427,6 +454,10 @@ class ischedule_server extends groupdav
|
|||||||
$public_key = self::dns_txt_pubkey($dkim['d'], $dkim['s']);
|
$public_key = self::dns_txt_pubkey($dkim['d'], $dkim['s']);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'http/well-known':
|
||||||
|
$public_key = self::well_known_pubkey($dkim['d'], $dkim['s']);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'private-exchange':
|
case 'private-exchange':
|
||||||
$public_key = self::private_exchange_pubkey($dkim['d'], $dkim['s']);
|
$public_key = self::private_exchange_pubkey($dkim['d'], $dkim['s']);
|
||||||
break;
|
break;
|
||||||
@ -437,13 +468,16 @@ class ischedule_server extends groupdav
|
|||||||
}
|
}
|
||||||
if ($public_key) break;
|
if ($public_key) break;
|
||||||
}
|
}
|
||||||
|
// for testing purpose allways try private-exchange, if none other match
|
||||||
|
if (!$public_key) $public_key = self::private_exchange_pubkey($dkim['d'], $dkim['s']);
|
||||||
|
|
||||||
if (!$public_key)
|
if (!$public_key)
|
||||||
{
|
{
|
||||||
$error = "No public key for d='$dkim[d]' and s='$dkim[s]' using methods q='$dkim[q]'";
|
$error = "No public key for d='$dkim[d]' and s='$dkim[s]' using methods q='$dkim[q]'";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$ok = openssl_verify($_unsigned, base64_decode($dkim['b']), $public_key, $hash_algo);
|
$ok = openssl_verify($_unsigned, base64_decode($dkim['b']), $public_key, $hash_algo);
|
||||||
//error_log(__METHOD__."() openssl_verify('$_unsigned', ..., '$public_key', '$hash_algo') returned ".array2string($ok));
|
error_log(__METHOD__."() openssl_verify('$_unsigned', ..., '$public_key', '$hash_algo') returned ".array2string($ok));
|
||||||
|
|
||||||
switch($ok)
|
switch($ok)
|
||||||
{
|
{
|
||||||
@ -456,7 +490,7 @@ class ischedule_server extends groupdav
|
|||||||
// if dkim did not validate, try not splitting Recipient header
|
// if dkim did not validate, try not splitting Recipient header
|
||||||
if (!isset($split_recipients))
|
if (!isset($split_recipients))
|
||||||
{
|
{
|
||||||
return $this->dkim_validate($headers, $body, $error, $required_headers, false);
|
return self::dkim_validate($headers, $body, $error, false);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -478,18 +512,26 @@ JPeajkWy/0CJn+d6rX/ncPMGX2EYzqXy/CyVqpcnVAosToymo6VHL6ufhzlyLJFD
|
|||||||
znLtV121CZLUZlAySQIDAQAB
|
znLtV121CZLUZlAySQIDAQAB
|
||||||
-----END PUBLIC KEY-----',
|
-----END PUBLIC KEY-----',
|
||||||
),
|
),
|
||||||
'bedework.org' => array(
|
'mysite.edu' => array(
|
||||||
'selector' => '-----BEGIN PUBLIC KEY-----
|
'selector' => '-----BEGIN PUBLIC KEY-----
|
||||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuv+6UtGUdPerJ3s0HCng2sv3c
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuv+6UtGUdPerJ3s0HCng2sv3c
|
||||||
R3ttma0JB6rMFfOTi1oHgk+h328MfGzhZK+SA9tsRPBcrJE/3uxs4SS2XNG9qRCG
|
R3ttma0JB6rMFfOTi1oHgk+h328MfGzhZK+SA9tsRPBcrJE/3uxs4SS2XNG9qRCG
|
||||||
0YMmNFOmubht4RhQhS9drSNyMZbhy2MPVbl9lHAJULFdaDdLj1hc3xTMWy8sDa8s
|
0YMmNFOmubht4RhQhS9drSNyMZbhy2MPVbl9lHAJULFdaDdLj1hc3xTMWy8sDa8s
|
||||||
M8r0gHvp/sPSe9CQQQIDAQAB
|
M8r0gHvp/sPSe9CQQQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----',
|
||||||
|
),
|
||||||
|
'caldav.egroupware.net' => array(
|
||||||
|
'calendar' => '-----BEGIN PUBLIC KEY-----
|
||||||
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiawhLuTSVhnl1zz5pXs1A748y
|
||||||
|
N3aNE181dni8nsYqIQB1h4H32J4dZurEiAnP9nflQRjCmmg1NTvFcNz11Bem4zo1
|
||||||
|
K4r4mcfbjlheorK2Mwoh445HR3fo/pP7uV6CcXTNboBJLTxs6ZHswmQjxyuKBKmx
|
||||||
|
yXUKsIQVi3qPyPdB3QIDAQAB
|
||||||
-----END PUBLIC KEY-----',
|
-----END PUBLIC KEY-----',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch public key from dns txt recored dkim q=dns/txt
|
* Fetch public key from dns txt recored dkim q=private-exchange
|
||||||
*
|
*
|
||||||
* @param string $d domain
|
* @param string $d domain
|
||||||
* @param string $s selector
|
* @param string $s selector
|
||||||
@ -504,6 +546,26 @@ M8r0gHvp/sPSe9CQQQIDAQAB
|
|||||||
return self::$private_exchange[$d][$s];
|
return self::$private_exchange[$d][$s];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch public key via http for q=http/well-known
|
||||||
|
*
|
||||||
|
* GET request to https://$domain/.well-known/domainkey/$selector
|
||||||
|
* Content should be identical to txt record for dns/txt.
|
||||||
|
*
|
||||||
|
* @param string $d domain
|
||||||
|
* @param string $s selector
|
||||||
|
* @return string|boolean string with (full) public key or false if not found or other error retrieving it
|
||||||
|
*/
|
||||||
|
public static function well_known_pubkey($d, $s)
|
||||||
|
{
|
||||||
|
if (!($keys = @file_get_contents('https://'.$d.'/.well-known/domainkey/'.$s)) ||
|
||||||
|
preg_match('/p=([^;]+)/i', $keys, $matches))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return "-----BEGIN PUBLIC KEY-----\n".chunk_split($matches[1], 64, "\n")."-----END PUBLIC KEY-----\n";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch public key from dns txt recored dkim q=dns/txt
|
* Fetch public key from dns txt recored dkim q=dns/txt
|
||||||
*
|
*
|
||||||
@ -684,7 +746,7 @@ M8r0gHvp/sPSe9CQQQIDAQAB
|
|||||||
$code = $e->getCode();
|
$code = $e->getCode();
|
||||||
$msg = $e->getMessage();
|
$msg = $e->getMessage();
|
||||||
if (!in_array($code, array(400, 403, 407, 503))) $code = 500;
|
if (!in_array($code, array(400, 403, 407, 503))) $code = 500;
|
||||||
header('HTTP/1.1 '.$code.' '.$msg);
|
header('HTTP/1.1 '.$code.' '.$msg, true, $code);
|
||||||
|
|
||||||
// if our groupdav logging is active, log the request plus a trace, if enabled in server-config
|
// if our groupdav logging is active, log the request plus a trace, if enabled in server-config
|
||||||
if (groupdav::$request_starttime && isset(self::$instance))
|
if (groupdav::$request_starttime && isset(self::$instance))
|
||||||
|
Loading…
Reference in New Issue
Block a user