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

452 lines
15 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;
}
/**
* 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;
}
//////////////////////
// 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
* @return string DKIM-Signature Header String
* @access public
* @author Ahmad Amarullah
*/
public function getDKIM($h,$_h,$body) {
// Relax Canonicalization for Body
$_b = $this->bodyRelaxCanon($body);
// Canonicalizated Body Length [tag:l]
$_l = strlen($_b);
// Signature Timestamp [tag:t]
$_t = time();
// Hash of the canonicalized body [tag:bh]
$_bh= base64_encode(sha1($_b,true));
#^--for ver < PHP5.3 # $_bh= base64_encode(pack("H*",sha1($_b)));
// Creating DKIM-Signature
$_dkim = "DKIM-Signature: ".
"v=1; ". // DKIM Version
"a=rsa-sha1; ". // The algorithm used to generate the signature "rsa-sha1"
"s={$this->s}; ". // The selector subdividing the namespace for the "d=" (domain) tag
"d={$this->d}; ". // The domain of the signing entity
"l={$_l}; ". // Canonicalizated Body length count
"t={$_t}; ". // Signature Timestamp
"c=relaxed/relaxed; ". // 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)
// Wrap DKIM Header
$_dkim = wordwrap($_dkim,76,"\r\n\t");
// Canonicalization Header Data
$_unsigned = $this->headRelaxCanon(implode("\r\n",$_h)."\r\n{$_dkim}");
// Sign Canonicalization Header Data with Private Key
openssl_sign($_unsigned, $_signed, $this->pkid, OPENSSL_ALGO_SHA1);
// 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}";
}
}
?>