<?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; } /** * 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; } /** * 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 * @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 */ public function getDKIM($h,$_h,$body,$_c='relaxed/relaxed',$_a='rsa-sha1',$_dkim=null) { // Relax Canonicalization for Body 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] // 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] list(,$hash_algo) = explode('-', $_a); $_bh = base64_encode(hash($hash_algo,$_b,true)); // Creating DKIM-Signature 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 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 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}"; } } ?>