OK, finally I have found a workaround.
I have a web page encoded in UTF-8 (this will be very important on some next steps);
Generate the signature:
If the user uses Mozilla/Firefox/Chrome - the signature is generated using the window.crypto
. For more information read this
If the user uses Internet Explorer - the signature is generated using the CAPICOM
(Crypto Application Interface COM object). For more information read read MSDN Both - window.crypto
and CAPICOM
are not very well documented for web using (CAPICOM
is not only for web!)
The text string and the signature are being sent to the web server by POST request.
The server (Linux, Apache and PHP) have to verify the signature.
Now the problems:
The openssl_verify()
function of PHP is also not well documented and always returns zero - signature not valid.
The CMD tool openssl
also is not validating the signature.
Because my web server requires SSL certificate authentication I wanted that the user signs the text string with the same certificate he is logged in.
So what is the workaround:
I found that CAPICOM
is converting the signed string to UTF-16LE before signing it. Unfortunately the web browser sends the text string to the web server encoded in UTF-8. It means you have to convert the string from UTF-8 to UTF-16LE before verifying the signature. But this is valid only if the signature was generated by CAPICOM
.
The openssl
CMD tool is working proper now. The difference between the CMD tool and the PHP function are:
The signature and the source are sent to PHP function as strings. In case of using the CMD tool the signature and the source are sent as file paths. So if you are using the CMD instead of PHP function you have first to save the signature and the source as files. Remember to convert the encoding if needed.
The PHP function expects as a parameter to receive the public key of the signer. So before this you have to check if the certificate is issued by a trusted Certificate Authority (CA). Instead - the CMD tool expects as a parameter to receive a file which contains a list of root certificates of trusted Certificate Authorities. This means - you have to check the signer before verifying.
It seems that under Firefox/Mozilla/Chrome browsers it is not possible to limit the user exactly which certificate to use for signing. But it is possible to limit the options. Of course this is not documented at all (or I didn't find any proper information about this). So the signText()
function expects a third parameter which must be the trusted Certificate Authority names. First I expected that this must be the CN of the issuer of the client's certificate. But actually it is not. It must be all the issuer string separated with commas like:
C=Country,ST=State,L=Location,O=Organization,CN=CommonName,STREET=Address
Unfortunately this string is slightly different from the issuer string from openssl
which looks like
issuer=/streetAddress=Address/CN=CommonName/O=Organization/L=Location/ST=State/C=Country.
If someone have a Firefox under Linux it will be very interesting to check if this string is formatted the same way. I don't know how to separate more CA names in a single string.
Under Internet Explorer, using CAPICOM
it is possible to send only one certificate object to the signing object so it will not open the dialog to select a certificate from a list. You can find the proper certificate by comparing the root certificate fingerprint (a SHA1 hash of the BASE64 65 chr/line encoded certificate excluding the header and footer) and the serial number of the certificate. Just read the MSDN it is well documented.
Now. What my source code looks like:
If the signature was generated by CAPICOM
of window.crypto
(I receive an additional parameter from the web browser how the signature was generated) If CAPICOM
is used I convert the source data like this:
$_POST['source'] = iconv('UTF-8', 'UTF-16LE', $_POST['source'])
I generate 2 temporary files. The names of the files can be generated using the PHP function uniqid()
. The default temp directory can be found using the sys_get_temp_dir()
.
The source file is saved exactly as received from the POST
array. If the signature was generated by CAPICOM
it must be converted to UTF-16LE.
The signature comes from the web browser BASE64 encoded. Do not decode it. Just save it in a new file and add a special header and footer like this:
"-----BEGIN PKCS7-----\n".$_POST['signature']."\n-----END PKSC7-----"
Note that the header and footer must be on separate lines. The lines separator must be only \n (ASCII #10) not \r (ASCII #13) or \r\n (ASCII #13#10).
You must have a file which contains all trusted CA root certificates. The format of this file must be as follows:
A header "-----BEGIN CERTIFICATE-----"
BASE64 encoded certificate
A footer "-----END CERTIFICATE-----"
empty line
If you have more than one trusted CA root - each certificate must start with a header and end with a footer string. All certificates are stored in a single file
Run the CMD tool openssl
using next parameters:
smime
- to use the SMIME function of the CMD tool
-verify
- to do a verification
-in filepath
- the path to the signature file created in step 4
-inform PEM
- the forat of the signature is BASE64 encoded file with header and footer
-binary
- prevents translation of the source from binary to text
-content filepath
- the path to the source file created in step 3
-CAfile filepath
- the path to the trusted CA root certificates file created in step 5
So the final command looks like this:
openssl smime -verify -in file.pem -inform PEM -binary -content source.txt -CAfile root.pem
Now call this from PHP using the shell_exec()
function and read the output.
If the output string starts with Verification successful - the signature is OK.
If the output string starts with Verification failure - the signature is not OK.
If the output string is different - some error have occurred. The error description is stored in the output string.
The above works for me. Unfortunately openssl
, CAPICOM
and window.crypto
are very tricky and it is always possible that a problem occurs. Hope this will help somebody.