mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-01-14 01:48:35 +01:00
dkim signature according to iSchedule draft 02
This commit is contained in:
parent
5a890e36ab
commit
18ceb54882
@ -160,10 +160,11 @@ class ischedule_client
|
||||
* @param string $content
|
||||
* @param string $content_type
|
||||
* @param boolean $debug=false true echo request before posting
|
||||
* @param int $max_redirect=3 maximum number of redirect before failing
|
||||
* @return string
|
||||
* @throws Exception with http status code and message, if server responds other then 2xx
|
||||
*/
|
||||
public function post_msg($content, $content_type, $debug=false)
|
||||
public function post_msg($content, $content_type, $debug=false, $max_redirect=3)
|
||||
{
|
||||
if (empty($this->dkim_private_key))
|
||||
{
|
||||
@ -173,6 +174,7 @@ class ischedule_client
|
||||
$headers = array(
|
||||
'Host' => $url_parts['host'].($url_parts['port'] ? ':'.$url_parts['port'] : ''),
|
||||
'iSchedule-Version' => self::VERSION,
|
||||
'iSchedule-Message-ID' => uniqid(),
|
||||
'Content-Type' => $content_type,
|
||||
'Originator' => $this->originator,
|
||||
'Recipient' => $this->recipient,
|
||||
@ -190,7 +192,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
|
||||
//'follow_location' => 1, // default 1=follow, but only for POST!
|
||||
//'timeout' => $timeout, // max timeout in seconds (float)
|
||||
'content' => $content,
|
||||
)
|
||||
@ -202,6 +204,24 @@ class ischedule_client
|
||||
if (($response = @file_get_contents($this->url, false, stream_context_create($opts))) === false)
|
||||
{
|
||||
list(, $code, $message) = explode(' ', $http_response_header[0], 3);
|
||||
if ($max_redirect && $code[0] === '3')
|
||||
{
|
||||
foreach($http_response_header as $header)
|
||||
{
|
||||
if (stripos($header, 'location:') === 0)
|
||||
{
|
||||
list(,$location) = preg_split('/: ?/', $header, 2);
|
||||
if ($location[0] == '/')
|
||||
{
|
||||
$parts = parse_url($this->url);
|
||||
$location = $parts['scheme'].'://'.$parts['host'].($parts['port'] ? ':'.$parts['port'] : '').$location;
|
||||
}
|
||||
$this->url = $location;
|
||||
// follow redirect
|
||||
return $this->post_msg($content, $content_type, $debug, $max_redirect-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Exception($message, $code);
|
||||
}
|
||||
return $response;
|
||||
@ -216,18 +236,32 @@ class ischedule_client
|
||||
* @param string $sign_headers='Content-Type:Host:Originator:Recipient'
|
||||
* @return string DKIM-Signature: ...
|
||||
*/
|
||||
public function dkim_sign(array $headers, $body, $selector='calendar', $sign_headers='Content-Type:Host:Originator:Recipient')
|
||||
public function dkim_sign(array $headers, $body, $selector='calendar')
|
||||
{
|
||||
$dkim_headers = array();
|
||||
foreach($headers as $header => $value)
|
||||
{
|
||||
$dkim_headers[] = $header.': '.$value;
|
||||
}
|
||||
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);
|
||||
|
||||
$dkim_headers = array();
|
||||
foreach(explode(':', $sign_headers) as $header)
|
||||
{
|
||||
$dkim_headers[] = $header.': '.$headers[$header];
|
||||
}
|
||||
$dkim = $mds->getDKIM(strtolower($sign_headers), $dkim_headers, $body);
|
||||
// generate DKIM signature according to iSchedule spec
|
||||
$dkim = $mds->getDKIM(implode(':', array_keys($headers)), $dkim_headers, $body, 'relaxed/simple', 'rsa/sha256',
|
||||
"DKIM-Signature: ".
|
||||
"v=1; ". // DKIM Version
|
||||
"a=\$a; ". // The algorithm used to generate the signature "rsa-sha1"
|
||||
"q=dns/txt:http/well-known; ". // how to fetch public key: dns/txt, http/well-known or private-exchange
|
||||
"x=300; ". // how long request will be valid in sec
|
||||
// end iSchedule specific
|
||||
"s=\$s; ". // The selector subdividing the namespace for the "d=" (domain) tag
|
||||
"d=\$d; ". // The domain of the signing entity
|
||||
"l=\$l; ". // Canonicalizated Body length count
|
||||
"t=\$t; ". // Signature Timestamp
|
||||
"c=\$c; ". // Message (Headers/Body) Canonicalization "relaxed/relaxed"
|
||||
"h=\$h; ". // Signed header fields
|
||||
"bh=\$bh;\r\n\t". // The hash of the canonicalized body part of the message
|
||||
"b="); // The signature data (Empty because we will calculate it later));
|
||||
|
||||
// as we do http, no need to fold dkim, in fact recommendation is not to
|
||||
$dkim = str_replace(array(";\r\n\t", "\r\n\t"), array('; ', ''), $dkim);
|
||||
|
@ -28,6 +28,11 @@ class ischedule_server
|
||||
*/
|
||||
const VERSION = '1.0';
|
||||
|
||||
/**
|
||||
* Required headers in DKIM signature (DKIM-Signature is always a required header!)
|
||||
*/
|
||||
const REQUIRED_DKIM_HEADERS = 'Content-Type:Host:Originator:Recipient';
|
||||
|
||||
/**
|
||||
* Serve an iSchedule request
|
||||
*/
|
||||
@ -76,17 +81,25 @@ class ischedule_server
|
||||
protected function post()
|
||||
{
|
||||
// get and verify required headers
|
||||
static $required_headers = array('Host','Recipient','Originator','Content-Type','DKIM-Signature');
|
||||
$headers = array();
|
||||
foreach($required_headers as $header)
|
||||
foreach($_SERVER as $name => $value)
|
||||
{
|
||||
$server_name = strtoupper(str_replace('-', '_', $header));
|
||||
if (strpos($server_name, 'CONTENT_') !== 0) $server_name = 'HTTP_'.$server_name;
|
||||
if (!empty($_SERVER[$server_name])) $headers[$header] = $_SERVER[$server_name];
|
||||
$name = strtolower(str_replace('_', '-', $name));
|
||||
list($first, $rest) = explode('-', $name, 2);
|
||||
switch($first)
|
||||
{
|
||||
case 'content':
|
||||
$headers[$name] = $value;
|
||||
break;
|
||||
case 'http':
|
||||
$headers[$rest] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (($missing = array_diff(array_keys($headers), $required_headers)))
|
||||
if (($missing = array_diff(explode(':', strtolower(self::REQUIRED_DKIM_HEADERS.':DKIM-Signature')), array_keys($headers))))
|
||||
{
|
||||
throw new Exception ('Bad Request: missing '.implode(', ', $missing).' header(s)', 400);
|
||||
error_log(array2string(array_keys($headers)));
|
||||
throw new Exception ('Bad Request: missing required headers: '.implode(', ', $missing), 400);
|
||||
}
|
||||
|
||||
// get raw request body
|
||||
@ -99,7 +112,7 @@ class ischedule_server
|
||||
}
|
||||
// check if recipient is a user
|
||||
// ToDo: multiple recipients
|
||||
if (!($account_id = $GLOBALS['egw']->accounts->name2id($headers['Recipient'], 'account_email')))
|
||||
if (!($account_id = $GLOBALS['egw']->accounts->name2id($headers['recipient'], 'account_email')))
|
||||
{
|
||||
throw new Exception('Bad Request: unknown recipient', 400);
|
||||
}
|
||||
@ -121,12 +134,12 @@ class ischedule_server
|
||||
}*/
|
||||
|
||||
// check method and component of Content-Type are valid
|
||||
if (!preg_match('/component=([^;]+)/i', $headers['Content-Type'], $matches) ||
|
||||
if (!preg_match('/component=([^;]+)/i', $headers['content-type'], $matches) ||
|
||||
(!in_array($component=strtoupper($matches[1]), self::$supported_components)))
|
||||
{
|
||||
throw new Exception ('Bad Request: missing or unsupported component in Content-Type header', 400);
|
||||
}
|
||||
if (!preg_match('/method=([^;]+)/i', $headers['Content-Type'], $matches) ||
|
||||
if (!preg_match('/method=([^;]+)/i', $headers['content-type'], $matches) ||
|
||||
(!isset(self::$supported_method2origin_requirement[$method=strtoupper($matches[1])])) ||
|
||||
$component == 'VFREEBUSY' && $method != 'REQUEST')
|
||||
{
|
||||
@ -159,11 +172,11 @@ class ischedule_server
|
||||
foreach($originator_requirement as $requirement)
|
||||
{
|
||||
if ($requirement == 'ORGANIZER' &&
|
||||
($event['organizer'] == $headers['Originator'] || strpos($event['organizer'], '<'.$headers['Originator'].'>') !== false) ||
|
||||
($event['organizer'] == $headers['originator'] || strpos($event['organizer'], '<'.$headers['originator'].'>') !== false) ||
|
||||
$requirement == 'ATTENDEE' &&
|
||||
(in_array('e'.$headers['Originator'], $event['participants']) ||
|
||||
(in_array('e'.$headers['originator'], $event['participants']) ||
|
||||
// ToDO: Participant could have CN, need to check that too
|
||||
$originator_account_id = $GLOBALS['egw']->accounts->name2id($headers['Originator'], 'account_email') &&
|
||||
$originator_account_id = $GLOBALS['egw']->accounts->name2id($headers['originator'], 'account_email') &&
|
||||
in_array($originator_account_id, $event['participants'])))
|
||||
{
|
||||
$matches = true;
|
||||
@ -278,35 +291,91 @@ class ischedule_server
|
||||
}
|
||||
}
|
||||
|
||||
const DKIM_HEADERS = 'content-type:host:originator:recipient';
|
||||
|
||||
/**
|
||||
* Validate DKIM signature
|
||||
*
|
||||
* @param array $headers
|
||||
* @param array $headers header-name in lowercase(!) as key
|
||||
* @param string $body
|
||||
* @param string &$error=null error if false returned
|
||||
* @return boolean true if signature could be validated, false otherwise
|
||||
* @todo
|
||||
* @todo other dkim q= methods: http/well-known bzw private-exchange
|
||||
*/
|
||||
public static function dkim_validate(array $headers, $body, &$error=null, $verify_headers='Content-Type:Host:Originator:Recipient')
|
||||
public static function dkim_validate(array $headers, $body, &$error=null, $required_headers=self::REQUIRED_DKIM_HEADERS)
|
||||
{
|
||||
// parse dkim siginature
|
||||
if (!isset($headers['DKIM-Signature']) ||
|
||||
!preg_match_all('/[\t\s]*([a-z]+)=([^;]+);?/i', $headers['DKIM-Signature'], $matches))
|
||||
if (!isset($headers['dkim-signature']) ||
|
||||
!preg_match_all('/[\t\s]*([a-z]+)=([^;]+);?/i', $headers['dkim-signature'], $matches))
|
||||
{
|
||||
$error = "Can't parse DKIM signature";
|
||||
return false;
|
||||
}
|
||||
$dkim = array_combine($matches[1], $matches[2]);
|
||||
|
||||
if (array_diff(explode(':', $dkim['h']), explode(':', strtolower($verify_headers))))
|
||||
if (($missing=array_diff(explode(':', strtolower($required_headers)), explode(':', strtolower($dkim['h'])))))
|
||||
{
|
||||
$error = "Missing required headers h=$dkim[h]";
|
||||
$error = "Missing required headers: ".implode(', ', $missing);
|
||||
return false;
|
||||
}
|
||||
|
||||
// fetch public key
|
||||
// create headers array
|
||||
$dkim_headers = array();
|
||||
foreach(explode(':', strtolower($dkim['h'])) as $header)
|
||||
{
|
||||
$dkim_headers[] = $header.': '.$headers[$header];
|
||||
}
|
||||
list($dkim_unsigned) = explode('b='.$dkim['b'], 'DKIM-Signature: '.$headers['dkim-signature']);
|
||||
$dkim_unsigned .= 'b=';
|
||||
|
||||
list($header_canon, $body_canon) = explode('/', $dkim['c']);
|
||||
require_once EGW_API_INC.'/php-mail-domain-signer/lib/class.mailDomainSigner.php';
|
||||
|
||||
// Canonicalization for Body
|
||||
switch($body_canon)
|
||||
{
|
||||
case 'relaxed':
|
||||
$_b = mailDomainSigner::bodyRelaxCanon($body);
|
||||
break;
|
||||
|
||||
case 'simple':
|
||||
$_b = mailDomainSigner::bodySimpleCanon($body);
|
||||
break;
|
||||
|
||||
default:
|
||||
$error = "Unknown body canonicalization '$body_canon'";
|
||||
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
|
||||
if ($_bh != $dkim['bh'])
|
||||
{
|
||||
$error = 'Body hash does NOT verify';
|
||||
error_log(__METHOD__."() body-hash='$_bh' != '$dkim[bh]'=dkim-bh $error");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Canonicalization Header Data
|
||||
switch($header_canon)
|
||||
{
|
||||
case 'relaxed':
|
||||
$_unsigned = mailDomainSigner::headRelaxCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned);
|
||||
break;
|
||||
|
||||
case 'simple':
|
||||
$_unsigned = mailDomainSigner::headSimpleCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned);
|
||||
break;
|
||||
|
||||
default:
|
||||
$error = "Unknown header canonicalization '$header_canon'";
|
||||
return false;
|
||||
}
|
||||
error_log(__METHOD__."() unsigned='$_unsigned'");
|
||||
|
||||
// fetch public key q=dns/txt method
|
||||
// ToDo other $dkim[q] methods: http/well-known bzw private-exchange
|
||||
if (!($dns = self::fetch_dns($dkim['d'], $dkim['s'])))
|
||||
{
|
||||
$error = "No public key for d='$dkim[d]' and s='$dkim[s]'";
|
||||
@ -314,19 +383,6 @@ class ischedule_server
|
||||
}
|
||||
$public_key = "-----BEGIN PUBLIC KEY-----\n".chunk_split($dns['p'], 64, "\n")."-----END PUBLIC KEY-----\n";
|
||||
|
||||
// create headers array
|
||||
$dkim_headers = array();
|
||||
foreach(explode(':', $verify_headers) as $header)
|
||||
{
|
||||
$dkim_headers[] = $header.': '.$headers[$header];
|
||||
}
|
||||
list($dkim_unsigned) = explode('b=', 'DKIM-Signature: '.$headers['DKIM-Signature']);
|
||||
$dkim_unsigned .= 'b=';
|
||||
|
||||
// Canonicalization Header Data
|
||||
require_once EGW_API_INC.'/php-mail-domain-signer/lib/class.mailDomainSigner.php';
|
||||
$_unsigned = mailDomainSigner::headRelaxCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned);
|
||||
|
||||
$ok = openssl_verify($_unsigned, base64_decode($dkim['b']), $public_key);
|
||||
|
||||
switch($ok)
|
||||
@ -341,19 +397,6 @@ class ischedule_server
|
||||
return false;
|
||||
}
|
||||
|
||||
// Relax Canonicalization for Body
|
||||
$_b = mailDomainSigner::bodyRelaxCanon($body);
|
||||
// Hash of the canonicalized body [tag:bh]
|
||||
$_bh= base64_encode(sha1($_b,true));
|
||||
|
||||
// check body hash
|
||||
if ($_bh != $dkim['bh'])
|
||||
{
|
||||
$error = 'Body hash does NOT verify';
|
||||
error_log(__METHOD__."() body-hash='$_bh' != '$dkim[bh]'=dkim-bh $error");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user