egroupware/phpgwapi/inc/class.mailDomainSigner.inc.php

587 lines
19 KiB
PHP
Raw Normal View History

<?php
/**
* Copyright 2011 Ahmad Amarullah
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/**
* mailDomainSigner - PHP class for Add DKIM-Signature and DomainKey-Signature on your mail
*
* Requirement: >= PHP 5.3.0
* For lower version, find "#^--for ver < PHP5.3" comment
*
* @link http://code.google.com/p/php-mail-domain-signer/
*
* @package mailDomainSigner
* @author Ahmad Amarullah
*/
class mailDomainSigner{
///////////////////////
// PRIVATE VARIABLES //
///////////////////////
private $pkid=null;
private $s;
private $d;
//////////////////////
// AGENT PROPERTIES //
//////////////////////
private $__app_name = "PHP mailDomainSigner";
private $__app_ver = "0.1-20110129";
private $__app_url = "http://code.google.com/p/php-mail-domain-signer/";
/**
* Constructor
* @param string $private_key Raw Private Key to Sign the mail
* @param string $d The domain name of the signing domain
* @param string $s The selector used to form the query for the public key
* @author Ahmad Amarullah
*/
public function __construct($private_key,$d,$s){
// Get a private key
$this->pkid = openssl_pkey_get_private($private_key);
// Save Domain and Selector
$this->d = $d;
$this->s = $s;
}
///////////////////////
// PRIVATE FUNCTIONS //
///////////////////////
/**
* The nofws ("No Folding Whitespace") Canonicalization Algorithm
* Function implementation according to RFC4870
*
* @link http://tools.ietf.org/html/rfc4870#page-19
* @param array $raw_headers Array of Mail Headers
* @param string $raw_body Raw Mail body
* @return string nofws Canonicalizated data
* @access public
* @author Ahmad Amarullah
*/
public function nofws($raw_headers,$raw_body){
// nofws-ed headers
$headers = array();
// Loop the raw_headers
foreach ($raw_headers as $header){
// Replace all Folding Whitespace
$headers[] = preg_replace('/[\r\t\n ]++/','',$header);
}
// Join headers with LF then Add it into data
$data = implode("\n",$headers)."\n";
// Loop Body Lines
foreach(explode("\n","\n".str_replace("\r","",$raw_body)) as $line)
{
// Replace all Folding Whitespace from current line
// then Add it into data
$data .= preg_replace('/[\t\n ]++/','',$line)."\n";
}
// Remove Trailing empty lines then split it with LF
$data = explode("\n",rtrim($data,"\n"));
// Join array of data with CRLF and Append CRLF
// to the resulting line
$data = implode("\r\n",$data)."\r\n";
// Return Canonicalizated Data
return $data;
}
/**
* The "relaxed" Header Canonicalization Algorithm
* Function implementation according to RFC4871
*
* Originally taken from RelaxedHeaderCanonicalization
* function in PHP-DKIM by Eric Vyncke
*
* @link http://tools.ietf.org/html/rfc4871#page-14
* @link http://php-dkim.sourceforge.net/
*
* @param string $s Header String to Canonicalization
* @return string Relaxed Header Canonicalizated data
* @access public
* @author Eric Vyncke
*/
public function headRelaxCanon($s) {
// Replace CR,LF and spaces into single SP
$s=preg_replace("/\r\n\s+/"," ",$s) ;
// Explode Header Line
$lines=explode("\r\n",$s) ;
// Loop the lines
foreach ($lines as $key=>$line) {
// Split the key and value
list($heading,$value)=explode(":",$line,2) ;
// Lowercase heading key
$heading=strtolower($heading);
// Compress useless spaces
$value=preg_replace("/\s+/"," ",$value);
// Don't forget to remove WSP around the value
$lines[$key]=$heading.":".trim($value);
}
// Implode it again
$s=implode("\r\n",$lines);
// Return Canonicalizated Headers
return $s;
}
2013-01-28 22:50:07 +01:00
/**
* The "ischedule-relaxed" Header Canonicalization Algorithm
* Function implementation according to draft-desruisseaux-ischedule-03:
*
* The "ischedule-relaxed" header canonicalization algorithm is used to
* canonicalize HTTP header fields where multiple headers fields with
* the same name might be combined by an HTTP intermediary
* into a single comma-separated header.
*
* Originally taken from headRelaxCanon above
*
* @param string $s Header String to Canonicalization
* @return string Relaxed Header Canonicalizated data
* @access public
* @author Ralf Becker
*/
public function headIScheduleRelaxCanon($s) {
// Replace CR,LF and spaces into single SP
$s=preg_replace("/\r\n\s+/"," ",$s) ;
// Loop exploded header lines
$lines=array();
foreach (explode("\r\n",$s) as $line) {
// Split the key and value
list($heading,$value)=explode(":",$line,2) ;
// Lowercase heading key
$heading=strtolower($heading);
// Compress useless spaces
$value=preg_replace("/\s+/"," ",$value);
// Don't forget to remove WSP around the value
$value = trim($value);
// remove whitespace after comma in values
$value = preg_replace("/,\s+/",",",$value);
// for multiple headers, add them comma-separated to existing headers
if (isset($lines[$heading])) {
$lines[$heading] .= ','.$value;
} else {
$lines[$heading] = $heading.':'.$value;
}
}
// Implode it again
$s=implode("\r\n",$lines);
// Return Canonicalizated Headers
return $s;
}
/**
* The "simple" Header Canonicalization Algorithm
*
* Simple canonicalzation means no change to headers!
*
* @link https://tools.ietf.org/html/rfc4871#section-3.4.1
*
* @param string $s Header String to Canonicalization
* @return string Simple Header Canonicalizated data
* @access public
*/
public function headSimpleCanon($s) {
return $s;
}
/**
* The "relaxed" Body Canonicalization Algorithm
* Function implementation according to RFC4871
*
* @link http://tools.ietf.org/html/rfc4871#page-15
*
* @param string $body Body String to Canonicalization
* @return string Relaxed Body Canonicalizated data
* @access public
* @author Ahmad Amarullah
*/
public function bodyRelaxCanon($body) {
// Return CRLF for empty body
if ($body == ''){
return "\r\n";
}
// Replace all CRLF to LF
$body = str_replace("\r\n","\n",$body);
// Replace LF to CRLF
$body = str_replace("\n","\r\n",$body);
// Ignores all whitespace at the end of lines
$body=rtrim($body,"\r\n");
// Canonicalizated String Variable
$canon_body = '';
// Split the body into lines
foreach(explode("\r\n",$body) as $line){
// Reduces all sequences of White Space within a line
// to a single SP character
$canon_body.= rtrim(preg_replace('/[\t\n ]++/',' ',$line))."\r\n";
}
// Return the Canonicalizated Body
return $canon_body;
}
2013-01-28 22:50:07 +01:00
/**
* The "simple" Body Canonicalization Algorithm
* Function implementation according to RFC4871
*
* @link https://tools.ietf.org/html/rfc6376#section-3.4.3
*
* @param string $body Body String to Canonicalization
* @return string Simple Body Canonicalizated data
* @access public
*/
public function bodySimpleCanon($body) {
// remove all empty lines (CRLF pairs) at the end of the body
while (substr($body, -2) === "\r\n") {
$body = substr($body, 0, -2);
}
// add a single CRLF at the end
$body .= "\r\n";
// Return the Canonicalizated Body
return $body;
}
//////////////////////
// PUBLIC FUNCTIONS //
//////////////////////
/**
* DKIM-Signature Header Creator Function
* implementation according to RFC4871
*
* Originally code inspired by AddDKIM
* function in PHP-DKIM by Eric Vyncke
* And rewrite it for better result
*
* The function use relaxed/relaxed canonicalization alghoritm
* for better verifing validation
*
* different from original PHP-DKIM that used relaxed/simple
* canonicalization alghoritm
*
* Doesn't include z, i and q tag for smaller data because
* it doesn't really needed
*
* @link http://tools.ietf.org/html/rfc4871
* @link http://php-dkim.sourceforge.net/
*
* @param string $h Signed header fields, A colon-separated list of header field names that identify the header fields presented to the signing algorithm
* @param array $_h Array of headers in same order with $h (Signed header fields)
* @param string $body Raw Email Body String
2013-01-28 22:50:07 +01:00
* @param string $_c='relaxed/relaxed' header/body canonicalzation algorithm, default "relaxed/relaxed", can also be any combination of "relaxed" or "simple"
* @param string $_a='rsa-sha1' could also be eg. 'rsa/sha256' and other hashes supported by openssl for rsa signing
* @param string $_dkim=null template for creating DKIM signature, default as for email appropriate
* @return string|boolean DKIM-Signature Header String, or false if an error happend, eg. unimplemented canonicalization requested
* @access public
* @author Ahmad Amarullah
*/
2013-01-28 22:50:07 +01:00
public function getDKIM($h,$_h,$body,$_c='relaxed/relaxed',$_a='rsa-sha1',$_dkim=null) {
// Relax Canonicalization for Body
2013-01-28 22:50:07 +01:00
list($header_canon,$body_canon) = explode('/',$_c);
switch($body_canon)
{
case 'relaxed':
$_b = $this->bodyRelaxCanon($body);
break;
case 'simple':
$_b = $this->bodySimpleCanon($body);
break;
default:
return false; // unknown/unimplemented canonicalzation algorithm
}
// Canonicalizated Body Length [tag:l]
2013-01-28 22:50:07 +01:00
// use mb_strlen if availble to get length in bytes/octets, to kope with evtl. set mbstring.func_overload!
$_l = function_exists('mb_strlen') ? mb_strlen($_b, '8bit') : strlen($_b);
// Signature Timestamp [tag:t]
$_t = time();
// Hash of the canonicalized body [tag:bh]
2013-01-28 22:50:07 +01:00
list(,$hash_algo) = explode('-', $_a);
$_bh = base64_encode(hash($hash_algo,$_b,true));
// Creating DKIM-Signature
2013-01-28 22:50:07 +01:00
if (is_null($_dkim)) {
$_dkim = "DKIM-Signature: ".
"v=1; ". // DKIM Version
"a=\$a; ". // The algorithm used to generate the signature "rsa-sha1"
"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)
}
$_dkim = strtr($_dkim, array(
'$a' => $_a,
'$s' => $this->s,
'$d' => $this->d,
'$l' => $_l,
'$t' => $_t,
'$c' => $_c,
'$h' => $h,
'$bh' => $_bh,
));
// Wrap DKIM Header
$_dkim = wordwrap($_dkim,76,"\r\n\t");
// Canonicalization Header Data
2013-01-28 22:50:07 +01:00
switch($header_canon)
{
case 'relaxed':
$_unsigned = $this->headRelaxCanon(implode("\r\n",$_h)."\r\n{$_dkim}");
break;
case 'ischedule-relaxed':
$_unsigned = $this->headIScheduleRelaxCanon(implode("\r\n",$_h)."\r\n{$_dkim}");
break;
case 'simple':
$_unsigned = $this->headSimpleCanon(implode("\r\n",$_h)."\r\n{$_dkim}");
break;
default:
return false; // unknown/unimplemented canonicalzation algorithm
}
error_log(__METHOD__."() unsigned='".str_replace(array("\r","\n"),array('\\r','\\n'),$_unsigned)."'");
// Sign Canonicalization Header Data with Private Key
2013-01-28 22:50:07 +01:00
openssl_sign($_unsigned, $_signed, $this->pkid, $hash_algo);
// Base64 encoded signed data
// Chunk Split it
// Then Append it $_dkim
$_dkim .= chunk_split(base64_encode($_signed),76,"\r\n\t");
// Return trimmed $_dkim
return trim($_dkim);
}
/**
* DomainKey-Signature Header Creator Function
* implementation according to RFC4870
*
* The function use nofws canonicalization alghoritm
* for better verifing validation
*
* NOTE: the $h and $_h arguments must be in right order
* if to header location upper the from header
* it should ordered like "to:from", don't randomize
* the order for better validating result.
*
* NOTE: if your DNS TXT contained g=*, remove it
*
* @link http://tools.ietf.org/html/rfc4870
*
* @param string $h Signed header fields, A colon-separated list of header field names that identify the header fields presented to the signing algorithm
* @param array $_h Array of headers in same order with $h (Signed header fields)
* @param string $body Raw Email Body String
* @return string DomainKey-Signature Header String
* @access public
* @author Ahmad Amarullah
*/
public function getDomainKey($h,$_h,$body){
// If $h = empty, dont add h tag into DomainKey-Signature
$hval = '';
if ($h)
$hval= "h={$h}; ";
// Creating DomainKey-Signature
$_dk = "DomainKey-Signature: ".
"a=rsa-sha1; ". // The algorithm used to generate the signature "rsa-sha1"
"c=nofws; ". // Canonicalization Alghoritm "nofws"
"d={$this->d}; ". // The domain of the signing entity
"s={$this->s}; ". // The selector subdividing the namespace for the "d=" (domain) tag
"{$hval}"; // If Exists - Signed header fields
// nofws Canonicalization for headers and body data
$_unsigned = $this->nofws($_h,$body);
// Sign nofws Canonicalizated Data with Private Key
openssl_sign($_unsigned, $_signed, $this->pkid, OPENSSL_ALGO_SHA1);
// Base64 encoded signed data
// Chunk Split it
$b = chunk_split(base64_encode($_signed),76,"\r\n\t");
// Append sign data into b tag in $_dk
$_dk.="b={$b}";
// Return Wrapped and trimmed $_dk
return trim(wordwrap($_dk,76,"\r\n\t"));
}
/**
* Auto Sign RAW Mail Data with DKIM-Signature
* and DomailKey-Signature
*
* It Support auto positioning Signed header fields
*
* @param string $mail_data Raw Mail Data to be signed
* @param string $suggested_h Suggested Signed Header Fields, separated by colon ":"
* Default: string("from:to:subject")
* @param bool $create_dkim If true, it will generate DKIM-Signature for $mail_data
* Default: boolean(true)
* @param bool $create_domainkey If true, it will generate DomailKey-Signature for $mail_data
* Default: boolean(true)
* @param integer $out_sign_header_only If true or 1, it will only return signature headers as String
* If 2, it will only return signature headers as Array
* If false or 0, it will return signature headers with original mail data as String
* Default: boolean(false)
* @return mixed Signature Headers with/without original data as String/Array depended on $out_sign_header_only parameter
* @access public
* @author Ahmad Amarullah
*/
public function sign(
$mail_data, // Raw Mail Data
$suggested_h = "from:to:subject", // Suggested Signed Header Fields
$create_dkim = true, // Create DKIM-Signature Header
$create_domainkey = true, // Create DomainKey-Signature Header
$out_sign_header_only = false // Return Signature Header Only without original data
){
if (!$suggested_h) $suggested_h = "from:to:subject"; // Default Suggested Signed Header Fields
// Remove all space and Lowercase Suggested Signed header fields then split it into array
$_h = explode(":",strtolower(preg_replace('/[\r\t\n ]++/','',$suggested_h)));
// Split Raw Mail data into $raw_headers and $body
list($raw_headers, $body) = explode("\r\n\r\n",$mail_data,2);
// Explode $raw_header into $header_list
$header_list = preg_split("/\r\n(?![\t ])/", $raw_headers);
// Empty Header Array
$headers = array();
// Loop $header_list
foreach($header_list as $header){
// Find Header Key for Array Key
list($key) = explode(':',$header, 2);
// Trim and Lowercase It
$key = strtolower(trim($key));
// If header with current key was exists
// Change it into array
if (isset($headers[$key])){
// If header not yet array set as Array
if (!is_array($headers[$key]))
$headers[$key] = array($headers[$key]);
// Add Current Header as next element
$headers[$key][] = $header;
}
// If header with current key not exists
// Insert header as string
else{
$headers[$key] = $header;
}
}
// Now, lets find accepted Suggested Signed header fields
// and reorder it to match headers position
$accepted_h = array(); // For Accepted Signed header fields
$accepted_headers = array(); // For Accepted Header
// Loop the Headers Array
foreach ($headers as $key=>$val){
// Check if $val wasn't array
// We don't want to include multiple headers as Signed header fields
if (!is_array($val)){
// Check if this header exists in Suggested Signed header fields
if (in_array($key,$_h)){
// If Exists, add it into accepted headers and accepted header fields
$accepted_h[] = $key;
$accepted_headers[] = $val;
}
}
}
// If it doesn't contain any $accepted_h
// return false, because we don't have enough data
// for signing email
if (count($accepted_h)==0)
return false;
// Create $_hdata for Signed header fields
// by imploding it with colon
$_hdata = implode(":",$accepted_h);
// New Headers Variable
$_nh = array("x-domain-signer"=>"X-Domain-Signer: {$this->__app_name} {$this->__app_ver} <$this->__app_url>");
// Create DKIM First
if ($create_dkim)
$_nh['dkim-signature'] = $this->getDKIM($_hdata,$accepted_headers,$body);
// Now Create Domain-Signature
if ($create_domainkey)
$_nh['domainKey-signature'] = $this->getDomainKey($_hdata,$accepted_headers,$body);
// Implode $_nh with \r\n
$to_be_appended_headers = implode("\r\n",$_nh);
// Return Immediately if
// * $out_sign_header_only=true (as headers string)
// * $out_sign_header_only=2 (as headers array)
if ($out_sign_header_only===2)
return $_nh;
elseif ($out_sign_header_only)
return "{$to_be_appended_headers}\r\n";
// Return signed headers with original data
return "{$to_be_appended_headers}\r\n{$mail_data}";
}
}
?>