got dkim-validation working with oversigned headers and sha256 hashing algorithm

This commit is contained in:
Ralf Becker 2012-10-05 10:52:53 +00:00
parent ed370717ad
commit 81376af3f3

View File

@ -35,6 +35,9 @@ class ischedule_server extends groupdav
*/ */
const REQUIRED_DKIM_HEADERS = 'iSchedule-Version:iSchedule-Message-ID:Content-Type:Originator:Recipient'; const REQUIRED_DKIM_HEADERS = 'iSchedule-Version:iSchedule-Message-ID:Content-Type:Originator:Recipient';
/**
* Constructor
*/
public function __construct() public function __construct()
{ {
// install our own exception handler sending exceptions as http status // install our own exception handler sending exceptions as http status
@ -119,12 +122,16 @@ class ischedule_server extends groupdav
} }
if (($missing = array_diff(explode(':', strtolower(self::REQUIRED_DKIM_HEADERS.':DKIM-Signature')), array_keys($headers)))) if (($missing = array_diff(explode(':', strtolower(self::REQUIRED_DKIM_HEADERS.':DKIM-Signature')), array_keys($headers))))
{ {
error_log(array2string(array_keys($headers))); //error_log('headers='.array2string(array_keys($headers)).', required='.self::REQUIRED_DKIM_HEADERS.', missing='.array($missing));
throw new Exception ('Bad Request: missing required headers: '.implode(', ', $missing), 400); throw new Exception ('Bad Request: missing required headers: '.implode(', ', $missing), 400);
} }
// validate dkim signature // validate dkim signature
if (!self::dkim_validate($headers, $this->request, $error)) // 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))
{ {
throw new Exception('Bad Request: DKIM signature invalid: '.$error, 400); throw new Exception('Bad Request: DKIM signature invalid: '.$error, 400);
} }
@ -312,15 +319,23 @@ error_log(array2string(array_keys($headers)));
/** /**
* Validate DKIM signature * Validate DKIM signature
* *
* For multivalued Recipient header(s): as PHP engine agregates them ", " separated,
* we can not tell these apart from ", " separated recipients in one header!
*
* Therefore we can only try to validate both situations.
*
* It will fail if multiple recipients in a single header are also ", " separated (just comma works fine).
*
* @param array $headers header-name in lowercase(!) as key * @param array $headers header-name in lowercase(!) as key
* @param string $body * @param string $body
* @param string &$error=null error if false returned * @param string &$error=null error if false returned
* @param boolean $split_recipients=null true=split recpients in multiple headers, false dont, default null try both
* @return boolean true if signature could be validated, false otherwise * @return boolean true if signature could be validated, false otherwise
* @todo other dkim q= methods: http/well-known bzw private-exchange * @todo other dkim q= methods: http/well-known bzw private-exchange
*/ */
public static function dkim_validate(array $headers, $body, &$error=null, $required_headers=self::REQUIRED_DKIM_HEADERS) public static function dkim_validate(array $headers, $body, &$error=null, $split_recipients=null)
{ {
// parse dkim siginature // parse dkim signature
if (!isset($headers['dkim-signature']) || if (!isset($headers['dkim-signature']) ||
!preg_match_all('/[\t\s]*([a-z]+)=([^;]+);?/i', $headers['dkim-signature'], $matches)) !preg_match_all('/[\t\s]*([a-z]+)=([^;]+);?/i', $headers['dkim-signature'], $matches))
{ {
@ -329,17 +344,26 @@ error_log(array2string(array_keys($headers)));
} }
$dkim = array_combine($matches[1], $matches[2]); $dkim = array_combine($matches[1], $matches[2]);
if (($missing=array_diff(explode(':', strtolower($required_headers)), explode(':', strtolower($dkim['h'])))))
{
$error = "Missing required headers: ".implode(', ', $missing);
return false;
}
// create headers array // create headers array
$dkim_headers = array(); $dkim_headers = array();
$check = $headers;
foreach(explode(':', strtolower($dkim['h'])) as $header) foreach(explode(':', strtolower($dkim['h'])) as $header)
{ {
$dkim_headers[] = $header.': '.$headers[$header]; // dkim oversigning: ommit not existing headers in signing
if (!isset($check[$header])) continue;
$value = $check[$header];
unset($check[$header]);
// special handling of multivalued recipient header
if ($header == 'recipient' && (!isset($split_recipients) || $split_recipients))
{
if (!is_array($value)) $value = explode(', ', $value);
$v = array_pop($value); // dkim uses reverse order!
if ($value) $check[$header] = $value;
$value = $v;
}
$dkim_headers[] = $header.': '.$value;
} }
list($dkim_unsigned) = explode('b='.$dkim['b'], 'DKIM-Signature: '.$headers['dkim-signature']); list($dkim_unsigned) = explode('b='.$dkim['b'], 'DKIM-Signature: '.$headers['dkim-signature']);
$dkim_unsigned .= 'b='; $dkim_unsigned .= 'b=';
@ -364,8 +388,8 @@ error_log(array2string(array_keys($headers)));
} }
// Hash of the canonicalized body [tag:bh] // Hash of the canonicalized body [tag:bh]
list(,$hash_algo) = explode('/', $dkim['a']); 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
if ($_bh != $dkim['bh']) if ($_bh != $dkim['bh'])
@ -386,22 +410,40 @@ error_log(array2string(array_keys($headers)));
$_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;
} }
error_log(__METHOD__."() unsigned='$_unsigned'");
// fetch public key q=dns/txt method // fetch public key using method in dkim q
// ToDo other $dkim[q] methods: http/well-known bzw private-exchange foreach(explode(':', $dkim['q']) as $method)
if (!($dns = self::fetch_dns($dkim['d'], $dkim['s'])))
{ {
$error = "No public key for d='$dkim[d]' and s='$dkim[s]'"; switch($method)
{
case 'dns/txt':
$public_key = self::dns_txt_pubkey($dkim['d'], $dkim['s']);
break;
case 'private-exchange':
$public_key = self::private_exchange_pubkey($dkim['d'], $dkim['s']);
break;
default: // not understood q method
$public_key = false;
break;
}
if ($public_key) break;
}
if (!$public_key)
{
$error = "No public key for d='$dkim[d]' and s='$dkim[s]' using methods q='$dkim[q]'";
return false; return false;
} }
$public_key = "-----BEGIN PUBLIC KEY-----\n".chunk_split($dns['p'], 64, "\n")."-----END PUBLIC KEY-----\n"; $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));
$ok = openssl_verify($_unsigned, base64_decode($dkim['b']), $public_key);
switch($ok) switch($ok)
{ {
@ -411,13 +453,73 @@ error_log(__METHOD__."() unsigned='$_unsigned'");
case 0: case 0:
$error = 'DKIM signature does NOT verify'; $error = 'DKIM signature does NOT verify';
error_log(__METHOD__."() unsigned='$_unsigned' $error"); // 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 false; return false;
} }
return true; return true;
} }
/**
* Provisional private-exchange public keys
*
* @var array domain => selector => public key
*/
static $private_exchange = array(
'example.com' => array(
'ischedule' => '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDtocSHvSS1Nn0uIL4Sg+0wp6Kc
W31WRC4Fww8P+jvsVAazVOxvxkShNSd18EvApiNa55P8WgKVEu02OQePjnjKNqfg
JPeajkWy/0CJn+d6rX/ncPMGX2EYzqXy/CyVqpcnVAosToymo6VHL6ufhzlyLJFD
znLtV121CZLUZlAySQIDAQAB
-----END PUBLIC KEY-----',
),
'bedework.org' => array(
'selector' => '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuv+6UtGUdPerJ3s0HCng2sv3c
R3ttma0JB6rMFfOTi1oHgk+h328MfGzhZK+SA9tsRPBcrJE/3uxs4SS2XNG9qRCG
0YMmNFOmubht4RhQhS9drSNyMZbhy2MPVbl9lHAJULFdaDdLj1hc3xTMWy8sDa8s
M8r0gHvp/sPSe9CQQQIDAQAB
-----END PUBLIC KEY-----',
),
);
/**
* Fetch public key from dns txt recored dkim q=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 private_exchange_pubkey($d, $s)
{
if (!isset(self::$private_exchange[$d]) || !isset(self::$private_exchange[$d][$s]))
{
return false;
}
return self::$private_exchange[$d][$s];
}
/**
* Fetch public key from dns txt recored dkim q=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 function dns_txt_pubkey($d, $s)
{
if (!($dns = self::fetch_dns($d, $s)))
{
return false;
}
return "-----BEGIN PUBLIC KEY-----\n".chunk_split($dns['p'], 64, "\n")."-----END PUBLIC KEY-----\n";
}
/** /**
* Fetch dns record and return parsed array * Fetch dns record and return parsed array
* *