How to extract and verify PDF signature (PKCS7) wi

2019-08-17 02:53发布

问题:

I would like to detect signed PDFs in PHP and verify if the signature is valid. From this document I have written this PHP code below.

What it does is:

  1. Extract the PKCS7 code (it works because I can get the details from Openssl)
  2. Compute the SHA256 hash of the document.

At the end I has a PKCS7 file and a SHA256.

Now, I would like to verify my signature against my PKCS7 file. How can I do this? I initially looked to the digest_enc_alg/sha256WithRSAEncryption/enc_digest, but it seems it is not what I am looking about.

class VerifyPDF
{
    public static function getByteRange($filename)
    {
        $content = file_get_contents($filename);
        if (!preg_match_all('/ByteRange\[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]/',
            $content, $matches))
        {
            throw new \Exception('Unable to get certificate');
        }

        return [
            intval($matches[1][0]), // Offset of the first part (usually 0)
            intval($matches[2][0]), // Size of the first part
            intval($matches[3][0]), // Offset to the second part
            intval($matches[4][0])  // Size of the second part
        ];
    }

    public static function get_pkcs7($filename)
    {
        [$o1, $l1, $o2, $l2] = self::getByteRange($filename);

        if (!$fp = fopen($filename, 'rb')) {
            throw new \Exception("Unable to open $filename");
        }

        $signature = stream_get_contents($fp, $o2 - $l1 - 2, $l1 + 1);
        fclose($fp);

        file_put_contents('out.pkcs7', hex2bin($signature));
    }

    public static function compute_hash($filename)
    {
        [$o1, $l1, $o2, $l2] = self::getByteRange($filename);

        if (!$fp = fopen($filename, 'rb')) {
            throw new \Exception("Unable to open $filename");
        }

        $i = stream_get_contents($fp, $l1, $o1);
        $j = stream_get_contents($fp, $l2, $o2);

        if (strlen($i) != $l1 || strlen($j) != $l2) {
            throw new \Exception('Invalid chunks');
        }

        fclose($fp);

        return hash('sha256', $i . $j);
    }
}

The HASH I get is:

5036ae43aba11ce626f6f9b1d5246ba0700e217655b9ff927e31fbefadfa2182

So inspired from this I did the following:

#!/bin/bash
PKCS7='out.pkcs7'

# Extract Digest (SHA256)
OFFSET=$(openssl asn1parse -inform der -in $PKCS7 | \
    perl -ne 'print $1 + $2 if /(\d+):d=\d\s+hl=(\d).*?256 prim.*HEX DUMP/m')
dd if=$PKCS7 of=signed-sha256.bin bs=1 skip=$OFFSET count=256

# Extract Public key 
openssl pkcs7 -print_certs -inform der -in $PKCS7 | \
    tac | sed '/-----BEGIN/q' | tac > client.pem
openssl x509 -in client.pem -pubkey -noout > client.pub.pem

# Verify the signature
openssl rsautl -verify -pubin -inkey client.pub.pem < signed-sha256.bin > verified.bin

# Get Hash and compare with the computed hash from the PDF
openssl asn1parse -inform der -in verified.bin | grep -Po '\[HEX DUMP\]:\K\w+$' | tr A-F a-f

Which gives me this:

C8581962753927BB57B66B1D0D0F4B33A29EF3E03DA12D2329DB72763AC7EDB6

So unfortunately by two hashes do not match...

Am I missing something?

回答1:

The blog you were inspired from shows the following graphics to explain the PKCS#7 signature container structure

Actually, though, this represents only the most simple structure defined by PKCS#7. If you look at the SignerInfo specification (content - signerInfos - SignerInfo), you'll see

   SignerInfo ::= SEQUENCE {
     version Version,
     issuerAndSerialNumber IssuerAndSerialNumber,
     digestAlgorithm DigestAlgorithmIdentifier,
     authenticatedAttributes
       [0] IMPLICIT Attributes OPTIONAL,
     digestEncryptionAlgorithm
       DigestEncryptionAlgorithmIdentifier,
     encryptedDigest EncryptedDigest,
     unauthenticatedAttributes
       [1] IMPLICIT Attributes OPTIONAL }

(RFC 2315 section 9.2 "SignerInfo type")

In particular there are the OPTIONAL authenticatedAttributes which you don't find in the sketch above. But in any current signature profile to be taken seriously these authenticatedAttributes (aka signed attributes) are actually required!

Furthermore, if there are authenticatedAttributes in a PKCS#7 signature container signer info object, the encrypted digest is not the digest of the document data but instead the digest of the authenticatedAttributes structure. In this case the digest of the document data is stored as the value of a specific signed attribute, the "messageDigest" attribute. Thus, in this case you try to extract the wrong value to compare the document digest with.

For example in case of the example document you shared in your follow-up question there are authenticatedAttributes, so the inspiring blog led you astray.