Using digital IDs in PHP scripts
This guide is intended for people who know how to program in PHP.
Digital IDs can also be used in PHP scripts. However the scripts require the components of the digital ID split into individual PEM files.
This guide is intended for people who know how to program in PHP.
Digital IDs can also be used in PHP scripts. However the scripts require the components of the digital ID split into individual PEM files.
This applies regardless of whether it is a digital ID for emails or a digital ID for PDF documents.
To do so, you have to use the OpenSSL software from the command prompt. On Linux and Macintosh computers this software should always be installed. Owners of Windows computers can download the software from www.openssl.org and install it.
If ID.p12
is the PKCS#12 file, you can use the
following commands to extract the components into the files
key.pem
, cert.pem
, and
chain.pem
.
openssl pkcs12 -in ID.p12 -out key.pem -nocerts
openssl pkcs12 -in ID.p12 -out cert.pem -nokeys -clcerts
openssl pkcs12 -in ID.p12 -out chain.pem -nokeys -cacerts
If you are using a newer OpenSSL version (from 3.0), but the digital ID is in traditional format, the above commands will not work, then please use the following commands:
openssl pkcs12 -legacy -in ID.p12 -out key.pem -nocerts
openssl pkcs12 -legacy -in ID.p12 -out cert.pem -nokeys -clcerts
openssl pkcs12 -legacy -in ID.p12 -out chain.pem -nokeys -cacerts
You will be asked for passwords several times because the private
key is first unpacked and then packed again. The new password will be
needed below as keypassword
.
For maximum portability, you should edit all files with a simple text editor:
From the file chain.pem
you should
remove the root certificate. You can recognize the root certificate by
the fact that issuer and subject are identical. It is possible that the
file is empty afterwards, then please observe the remarks in the guides
below.
From all files you should remove all comments (the texts outside of the BEGIN...END blocks) and insert empty lines between the blocks.
Numerous PHP functions accept a stream context. This stream context should contain these details, among others:
$context=stream_context_create(array(
'ssl' => array(
# verify server certificate
'verify_peer' => true,
'cafile' => realpath('roots.pem'),
'verify_depth' => 3, # minimum for DFN-PKI
# present our own certificate
'local_cert' => realpath('client.pem'),
'passphrase' => 'keypassword',
),
));
client.pem
is a PEM file containing the
contents of key.pem
,
cert.pem
, and chain.pem
in this order, separated by empty lines.
roots.pem
is a PEM file containing root
certificates and should contain at least the root certificate relevant
for the server to be contacted. This file contains all root and CA
certificates of the DFN-PKI, further root certificates can be extracted
from every WWW browser.
If you are using the TCPDF library to create a PDF document, only one additional statement is required in your PHP script, like this:
$pdf=new TCPDF(.....);
...
$pdf->setSignature(
or
'file://' . realpath('cert.pem'),
'file://' . realpath('key.pem'),
'keypassword',
realpath('chain.pem') #null
);
...
$pdf->Output(.....)
If the file chain.pem
is empty, as with the
digital IDs of our PDF-CA,
realpath('chain.pem')
must be replaced with
null
.
Using realpath()
is urgently recommended even if you
give absolute paths.
After including an additional function signmail()
only
one additional statement is required in your PHP script just before
calling the function mail()
to add the signature to the
contents already prepared for mail()
, and to encrypt the
whole mail, like this:
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
);
If the file chain.pem
is empty, replace
'chain.pem'
with null
.
If you want to encrypt the email, the files
cert1.pem
, cert2.pem
,
... must contain the certificates of the recipients. You can specify
any number of certificates.
If you want to sign only, simply omit the argument or do not specify
any certificate: array()
In place of the file names you can give the PEM data directly, then
the data must begin with -----BEGIN
.
The function signmail()
looks under Unix like this, it
may freely be used and distributed:
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;
}