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.
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 digital-id.p12
is the PKCS#12 file, you can use the
following commands to extract the components into the files
private-key.pem
,
certificate.pem
, and
chain.pem
.
openssl pkcs12 -in digital-id.p12 -out private-key.pem -nocerts
openssl pkcs12 -in digital-id.p12 -out certificate.pem -nokeys -clcerts
openssl pkcs12 -in digital-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 private-key-password
.
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' => 'private-key-password',
),
));
client.pem
is a PEM file containing the
contents of private-key.pem
,
certificate.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.
Only one additional statement is required in your PHP script, like this:
$pdf=new TCPDF(.....);
...
$pdf->setSignature(
'file://' . realpath('certificate.pem'),
'file://' . realpath('private-key.pem'),
'private-key-password',
realpath('chain.pem')
);
...
$pdf->Output(.....)
Using realpath()
is urgently recommended even if you
give absolute paths.
If the file chain.pem
is empty, replace
realpath('chain.pem')
with
null
.
According to https://www.pdf-insecurity.org/, most PDF readers, even from Adobe, do not check electronic signatures correctly. There are numerous diffent ways to trick these PDF readers into displaying forged content as genuine!
Obviously PDF internal signatures suffer from conceptual weaknesses. We therefore recommend to no longer trust PDF internal signatures at all.
Exceptions are possible if – as in the university administration – software is used for signature verification that is insensitive to all attacks mentioned on https:/www.pdf-insecurity.org. This must be checked again after each update of this website or the software.
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,
'private-key.pem',
'private-key-password',
'certificate.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,
$ownkey,
$keypass,
$owncert,
$ownchain=null,
$rcptcerts=null
){
# own key
if(substr($ownkey,0,10)!='-----BEGIN'){
$ownkey=realpath($ownkey);
if($ownkey===false)return false;
$ownkey='file://'.$ownkey;
}
# own certificate
if(substr($owncert,0,10)!='-----BEGIN'){
$owncert=realpath($owncert);
if($owncert===false)return false;
$owncert='file://'.$owncert;
}
# intermediate CA certificates
$chainsave='';
if(!$ownchain){
$ownchain=null;
}elseif(is_string($ownchain) and substr($ownchain,0,10)!='-----BEGIN'){
# a single file name
$ownchain=realpath($ownchain);
if($ownchain===false)return false;
# no prefix 'file://'
}else{
# collect work file
if(!is_array($ownchain))$ownchain=array($ownchain);
foreach($ownchain 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,$ownchain)){
@unlink($name1);
@unlink($name2);
@unlink($name3);
return false;
}
$ownchain=$name3;
}
# sign
if((
!file_put_contents(
$name1,
implode("\n",$cont)."\n\n".$body
)
)or(
!openssl_pkcs7_sign(
$name1,
$name2,
$owncert,
array($ownkey,$keypass),
# add headers here when only signing
$encrypt ? null : $head,
PKCS7_DETACHED,
$ownchain
)
)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;
}