forked from extern/egroupware
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';
|
||||
|
||||
/**
|
||||
* 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 $recipient;
|
||||
/**
|
||||
* Recipient email addresses
|
||||
*
|
||||
* @param array
|
||||
*/
|
||||
private $recipients;
|
||||
|
||||
/**
|
||||
* Originator email address
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $originator;
|
||||
|
||||
/**
|
||||
* Private key of originators domain
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $dkim_private_key;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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'];
|
||||
|
||||
if (is_null($url))
|
||||
@ -177,13 +199,17 @@ class ischedule_client
|
||||
'iSchedule-Message-ID' => uniqid(),
|
||||
'Content-Type' => $content_type,
|
||||
'Originator' => $this->originator,
|
||||
'Recipient' => $this->recipient,
|
||||
'Recipient' => $this->recipients,
|
||||
'Cache-Control' => 'no-cache, no-transform', // required by iSchedule spec
|
||||
'Content-Length' => bytes($content),
|
||||
);
|
||||
$header_string = '';
|
||||
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";
|
||||
|
||||
@ -192,7 +218,7 @@ class ischedule_client
|
||||
'method' => 'POST',
|
||||
'header' => $header_string,
|
||||
'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)
|
||||
'content' => $content,
|
||||
)
|
||||
@ -233,21 +259,30 @@ class ischedule_client
|
||||
* @param array $headers name => value pairs, names as in $sign_headers
|
||||
* @param string $body
|
||||
* @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: ...
|
||||
*/
|
||||
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();
|
||||
foreach($headers as $header => $value)
|
||||
$header_values = $header_names = array();
|
||||
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';
|
||||
list(,$domain) = explode('@', $this->originator);
|
||||
$mds = new mailDomainSigner($this->dkim_private_key, $domain, $selector);
|
||||
// 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: ".
|
||||
"v=1; ". // DKIM Version
|
||||
"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!)
|
||||
*/
|
||||
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
|
||||
@ -130,8 +131,7 @@ class ischedule_server extends groupdav
|
||||
// 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.
|
||||
// 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) &&
|
||||
!self::dkim_validate($headers, $this->request, $error, false))
|
||||
if (!self::dkim_validate($headers, $this->request, $error))
|
||||
{
|
||||
throw new Exception('Bad Request: DKIM signature invalid: '.$error, 400);
|
||||
}
|
||||
@ -315,6 +315,14 @@ class ischedule_server extends groupdav
|
||||
$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
|
||||
@ -344,6 +352,20 @@ class ischedule_server extends groupdav
|
||||
}
|
||||
$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
|
||||
$dkim_headers = array();
|
||||
$check = $headers;
|
||||
@ -365,10 +387,11 @@ class ischedule_server extends groupdav
|
||||
}
|
||||
$dkim_headers[] = $header.': '.$value;
|
||||
}
|
||||
list($dkim_unsigned) = explode('b='.$dkim['b'], 'DKIM-Signature: '.$headers['dkim-signature']);
|
||||
$dkim_unsigned .= 'b=';
|
||||
// dkim signature is obvious without content of signature, but must not necessarly be last tag
|
||||
$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';
|
||||
|
||||
// Canonicalization for Body
|
||||
@ -387,8 +410,15 @@ class ischedule_server extends groupdav
|
||||
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]
|
||||
list(,$hash_algo) = explode('-', $dkim['a']);
|
||||
$_bh = base64_encode(hash($hash_algo, $_b, true));
|
||||
|
||||
// check body hash
|
||||
@ -410,9 +440,6 @@ class ischedule_server extends groupdav
|
||||
$_unsigned = mailDomainSigner::headSimpleCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned);
|
||||
break;
|
||||
|
||||
case 'http/well-known':
|
||||
// todo
|
||||
|
||||
default:
|
||||
$error = "Unknown header canonicalization '$header_canon'";
|
||||
return false;
|
||||
@ -427,6 +454,10 @@ class ischedule_server extends groupdav
|
||||
$public_key = self::dns_txt_pubkey($dkim['d'], $dkim['s']);
|
||||
break;
|
||||
|
||||
case 'http/well-known':
|
||||
$public_key = self::well_known_pubkey($dkim['d'], $dkim['s']);
|
||||
break;
|
||||
|
||||
case 'private-exchange':
|
||||
$public_key = self::private_exchange_pubkey($dkim['d'], $dkim['s']);
|
||||
break;
|
||||
@ -437,13 +468,16 @@ class ischedule_server extends groupdav
|
||||
}
|
||||
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)
|
||||
{
|
||||
$error = "No public key for d='$dkim[d]' and s='$dkim[s]' using methods q='$dkim[q]'";
|
||||
return false;
|
||||
}
|
||||
$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)
|
||||
{
|
||||
@ -456,7 +490,7 @@ class ischedule_server extends groupdav
|
||||
// if dkim did not validate, try not splitting Recipient header
|
||||
if (!isset($split_recipients))
|
||||
{
|
||||
return $this->dkim_validate($headers, $body, $error, $required_headers, false);
|
||||
return self::dkim_validate($headers, $body, $error, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -478,18 +512,26 @@ JPeajkWy/0CJn+d6rX/ncPMGX2EYzqXy/CyVqpcnVAosToymo6VHL6ufhzlyLJFD
|
||||
znLtV121CZLUZlAySQIDAQAB
|
||||
-----END PUBLIC KEY-----',
|
||||
),
|
||||
'bedework.org' => array(
|
||||
'mysite.edu' => array(
|
||||
'selector' => '-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuv+6UtGUdPerJ3s0HCng2sv3c
|
||||
R3ttma0JB6rMFfOTi1oHgk+h328MfGzhZK+SA9tsRPBcrJE/3uxs4SS2XNG9qRCG
|
||||
0YMmNFOmubht4RhQhS9drSNyMZbhy2MPVbl9lHAJULFdaDdLj1hc3xTMWy8sDa8s
|
||||
M8r0gHvp/sPSe9CQQQIDAQAB
|
||||
-----END PUBLIC KEY-----',
|
||||
),
|
||||
'caldav.egroupware.net' => array(
|
||||
'calendar' => '-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiawhLuTSVhnl1zz5pXs1A748y
|
||||
N3aNE181dni8nsYqIQB1h4H32J4dZurEiAnP9nflQRjCmmg1NTvFcNz11Bem4zo1
|
||||
K4r4mcfbjlheorK2Mwoh445HR3fo/pP7uV6CcXTNboBJLTxs6ZHswmQjxyuKBKmx
|
||||
yXUKsIQVi3qPyPdB3QIDAQAB
|
||||
-----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 $s selector
|
||||
@ -504,6 +546,26 @@ M8r0gHvp/sPSe9CQQQIDAQAB
|
||||
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
|
||||
*
|
||||
@ -684,7 +746,7 @@ M8r0gHvp/sPSe9CQQQIDAQAB
|
||||
$code = $e->getCode();
|
||||
$msg = $e->getMessage();
|
||||
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 (groupdav::$request_starttime && isset(self::$instance))
|
||||
|
Loading…
Reference in New Issue
Block a user