Signierte oder signierte und verschlüsselte
E-Mail versenden
Nach Einbindung einer zusätzlichen Funktion
signmail()
wird nur eine zusätzliche Anweisung in
Ihrem PHP-Skript unmittelbar vor dem Aufruf der Funktion
mail()
benötigt, um die für mail()
bereits passend vorbereiteten Inhalte um die Signatur zu ergänzen
und um die ganze E-Mail zu verschlüsseln, etwa so:
signmail(
$message,
$additional_headers,
'key.pem',
'keypassword',
'cert.pem',
'chain.pem',
array('cert1.pem','cert2.pem',...)
) and mail(
$to,
$subject,
$message,
$additional_headers,
$additional_parameters
);
Falls die Datei chain.pem
leer ist, ersetzen
Sie 'chain.pem'
durch null
.
Falls Sie die E-Mail verschlüsseln möchten, müssen
die Dateien cert1.pem
,
cert2.pem
, ... die Zertifikate der
Empfänger enthalten. Es können beliebig viele
Empfängerzertifikate angegeben werden.
Falls Sie nur signieren möchten, lassen Sie das Argument
einfach weg oder geben Sie einfach gar kein Empfängerzertifikat
an: array()
Statt der Dateinamen können Sie auch die PEM-Daten direkt
angeben, diese müssen dann unmittelbar mit -----BEGIN
anfangen.
Die Funktion signmail()
sieht unter Unix so aus; sie
darf frei verwendet und verbreitet werden:
function signmail(
&$message,
&$additional_headers,
$key,
$keypass,
$cert,
$chain=null,
$rcptcerts=null
){
# own key
if(substr($key,0,10)!='-----BEGIN'){
$key=realpath($key);
if($key===false)return false;
$key='file://'.$key;
}
# own certificate
if(substr($cert,0,10)!='-----BEGIN'){
$cert=realpath($cert);
if($cert===false)return false;
$cert='file://'.$cert;
}
# intermediate CA certificates
$chainsave='';
if(!$chain){
$chain=null;
}elseif(is_string($chain) and substr($chain,0,10)!='-----BEGIN'){
# a single file name
$chain=realpath($chain);
if($chain===false)return false;
# no prefix 'file://'
}else{
# collect work file
if(!is_array($chain))$chain=array($chain);
foreach($chain as $onechain){
if(substr($onechain,0,10)!='-----BEGIN'){
$onechain=realpath($onechain);
if($onechain===false)return false;
$onechain=file_get_contents($onechain);
if($onechain===false)return false;
}
$chainsave.=$onechain;
$chainsave.="\n";
}
}
# if encrypting:
# certificates of recipients
$encrypt=array();
if($rcptcerts!==null){
if(!is_array($rcptcerts))$rcptcerts=array($rcptcerts);
foreach($rcptcerts as $onecert){
if(substr($onecert,0,10)!='-----BEGIN'){
$onecert=realpath($onecert);
if($onecert===false)return false;
$onecert='file://'.$onecert;
}
$encrypt[]=$onecert;
}
}
# prepare additional headers
# separate MIME content from other headers
$work=array();
foreach(explode("\n",trim($additional_headers)) as $line){
$line=chop($line);
if($line!=''){
if(in_array(substr($line,0,1),array(" ","\t"))){
$line=array_pop($work)."\n".$line;
}
$work[]=$line;
}
}
$head=array();
$cont=array();
foreach($work as $line){
if(strtolower(substr($line,0,13))=='mime-version:'){
# drop, we add our own
}elseif(strtolower(substr($line,0,8))=='content-'){
$cont[]=$line;
}else{
$head[]=$line;
}
}
# if no structured body yet, prepare as plain UTF-8 text
if(!$cont){
$cont[]='Content-Type: text/plain; charset=utf-8';
$cont[]='Content-Transfer-Encoding: quoted-printable';
$body='';
foreach(explode("\n",chop(
mb_check_encoding($message,'UTF-8')
? $message
# assume non-UTF-8 to be ISO-8859-1 or Windows-1252
: mb_convert_encoding($message,'UTF-8','Windows-1252')
)) as $line)$body.=strtr(
# to encode spaces correctly,
# quoted_printable_encode() requires CRLF line ends
quoted_printable_encode(
chop($line,"\r\n")."\r\n"
),
array("\r\n"=>"\n")
);
}else{
$body=$message;
}
# work files
$name1=tempnam(sys_get_temp_dir(),'signmail.1.');
$name2=tempnam(sys_get_temp_dir(),'signmail.2.');
# if given as string, save chain to file
if($chainsave!=''){
$name3=tempnam(sys_get_temp_dir(),'signmail.3.');
if(!file_put_contents($name3,$chain)){
@unlink($name1);
@unlink($name2);
@unlink($name3);
return false;
}
$chain=$name3;
}
# sign
if((
!file_put_contents(
$name1,
implode("\n",$cont)."\n\n".$body
)
)or(
!openssl_pkcs7_sign(
$name1,
$name2,
$cert,
array($key,$keypass),
# add headers here when only signing
$encrypt ? null : $head,
PKCS7_DETACHED,
$chain
)
)or(
($work=file_get_contents($name2))==''
)){
if($chainsave!='')@unlink($name3);
@unlink($name2);
@unlink($name1);
return false;
}
if($chainsave!='')@unlink($name3);
# encrypt
if($encrypt){
# PHP does not support GCM ciphers, we cannot avoid CBC.
# Mitigate EFail CBC/CFB Gadget Attack by prepending a header field
# with random length and random name ('X' + up to 70 characters).
# 70*log62(256) = 52.099 so use up to 52 random bytes with Base62.
# 70*log16(256) = 35.000 so use up to 35 random bytes with Base16.
# But use at least 16*8 = 128 bits of randomness.
if((
!file_put_contents(
$name1,'X'.(
extension_loaded('gmp') # Base62 available?
?gmp_strval('0x'.bin2hex(random_bytes(random_int(16,52))),62)
:bin2hex(random_bytes(random_int(16,35)))
).":\n".$work
)
)or(
!openssl_pkcs7_encrypt(
$name1,
$name2,
$encrypt,
# add headers here when also encrypting
$head,
0,
OPENSSL_CIPHER_AES_256_CBC
)
)or(
($work=file_get_contents($name2))==''
)){
@unlink($name2);
@unlink($name1);
return false;
}
}
# clean up
@unlink($name2);
@unlink($name1);
# result (call by reference)
list($additional_headers,$message)=explode(
"\n\n",
strtr($work,array("\r\n"=>"\n")),
2
);
return true;
}