I am using properly signed PDF and standard libraries to check message digest.
signerInformation.getContentDigest() return digest value different from decripted digestInfo.getDigest() value. In addition, signature verification fails on properly signed PDF file.
Went through all possible questions about digital signature.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import javax.crypto.Cipher;
import javax.xml.bind.DatatypeConverter;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x509.DigestInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationVerifier;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Store;
public class PDFSignatureValidatorSample {
public static void verifyPDF(byte[] doc) throws Exception {
PDDocument document = PDDocument.load(doc);
List signatures = document.getSignatureDictionaries();
PDSignature sig = signatures.get(0);
if (sig != null) {
String subFilter = sig.getSubFilter();
if (subFilter != null) {
Collection certs = new ArrayList();
switch (subFilter) {
case "ETSI.CAdES.detached":
case "adbe.pkcs7.detached":
byte[] signatureContent = sig.getContents(doc);
System.out.println("---------signatureContent length------------");
System.out.println(signatureContent.length);
String signatureContentB64 = Base64.getEncoder().encodeToString(signatureContent);
// System.out.println("---------signatureContent b64------------");
// System.out.println("signatureContentB64);
byte[] signedContent = sig.getSignedContent(doc);
String signedContentB64 = Base64.getEncoder().encodeToString(signedContent);
System.out.println("---------signedContent length------------");
System.out.println(signedContent.length);
// System.out.println("---------signedContent b64------------");
// System.out.println(signedContentB64);
// Now we construct a PKCS #7 or CMS.
CMSProcessable cmsProcessableInputStream = new CMSProcessableByteArray(signedContent);
CMSSignedData cmsSignedData = new CMSSignedData(cmsProcessableInputStream, signatureContent);
Store certificatesStore = cmsSignedData.getCertificates();
Collection signers = cmsSignedData.getSignerInfos().getSigners();
SignerInformation signerInformation = signers.iterator().next();
Collection matches = certificatesStore.getMatches(signerInformation.getSID());
X509CertificateHolder certificateHolder = (X509CertificateHolder) matches.iterator().next();
certificateHolder.getSerialNumber();
X509Certificate certFromSignedData = new JcaX509CertificateConverter()
.getCertificate(certificateHolder);
certs.add(certFromSignedData);
SignerInformationVerifier signerInformationVerifier = new JcaSimpleSignerInfoVerifierBuilder()
.build(certificateHolder);
boolean isValid = signerInformation.verify(signerInformationVerifier);
System.out.println("---------isValid - checked by signerInformation ------------");
System.out.println(isValid);
System.out.println("---------certSerialNumber dec------------");
System.out.println(certificateHolder.getSerialNumber());
System.out.println("---------certSerialNumber hex------------");
System.out.println(String.format("0x%08X", certificateHolder.getSerialNumber()));
System.out.println("---------getContentType------------");
System.out.println(signerInformation.getContentType().toString());
System.out.println("---------contentDigest base64------------");
byte[] contentDigest = signerInformation.getContentDigest();
String contentDigestB64 = Base64.getEncoder().encodeToString(contentDigest);
System.out.println(contentDigestB64);
System.out.println("---------contentDigest hex------------");
String contentDigestHex = DatatypeConverter.printHexBinary(contentDigest);
System.out.println(contentDigestHex);
System.out.println("---------digestAlgOID------------");
System.out.println(signerInformation.getDigestAlgOID());
System.out.println(signerInformation.getDigestAlgorithmID());
System.out.println("---------encryptionAlgOID------------");
System.out.println(signerInformation.getEncryptionAlgOID());
byte[] signatureBytes = signerInformation.getSignature();
String signatureBytesB64 = Base64.getEncoder().encodeToString(signatureBytes);
System.out.println("---------getSignature (encrypted) base64------------");
System.out.println(signatureBytesB64);
System.out.println("---------getSignature (encrypted) hex------------");
String signatureBytesHex = DatatypeConverter.printHexBinary(signatureBytes);
System.out.println(signatureBytesHex);
Cipher encryptCipher = Cipher.getInstance("RSA");
PublicKey publicKey = certFromSignedData.getPublicKey();
byte[] publicKeyBytes = publicKey.getEncoded();
String publicKeyBytesB64 = Base64.getEncoder().encodeToString(publicKeyBytes);
String publicKeyHex = DatatypeConverter.printHexBinary(publicKeyBytes);
System.out.println("---------publicKey base64------------");
System.out.println(publicKeyBytesB64);
System.out.println("---------publicKey hex------------");
System.out.println(publicKeyHex);
encryptCipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] cipherText = encryptCipher.doFinal(signatureBytes);
String cipherTextB64 = Base64.getEncoder().encodeToString(cipherText);
System.out.println("---------getSignature (decrypted) base64------------");
System.out.println(cipherTextB64);
System.out.println("---------getSignature (decrypted) hex------------");
String cipherTextHex = DatatypeConverter.printHexBinary(cipherText);
System.out.println(cipherTextHex);
byte[] digest = null;
ASN1InputStream ais = new ASN1InputStream(cipherText);
ASN1Primitive obj = ais.readObject();
DigestInfo digestInfo = new DigestInfo((ASN1Sequence) obj);
System.out.println("---------getAlgorithmId------------");
System.out.println(digestInfo.getAlgorithmId().getAlgorithm().getId());
System.out.println("---------getDigest hex------------");
digest = digestInfo.getDigest();
String digestHex = DatatypeConverter.printHexBinary(digest);
System.out.println(digestHex);
ais.close();
final Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(digest);
boolean signatureVerified = signature.verify(signatureBytes);
System.out.println("---------signature.verify------------");
System.out.println(signatureVerified);
Security.addProvider(new BouncyCastleProvider());
Signature bcSignature = Signature.getInstance("RSA", "BC");
bcSignature.initVerify(publicKey);
bcSignature.update(digest);
boolean signatureVerifiedBC = bcSignature.verify(signatureBytes);
System.out.println("---------signature bc.verify------------");
System.out.println(signatureVerifiedBC);
if (digestHex != contentDigestHex) {
System.out.println("--------------------------------------");
System.out.println("---------VERIFICATION FAILED---------");
System.out.println("---------calculated digest------------");
System.out.println(contentDigestHex);
System.out.println("---------decrypted digest ------------");
System.out.println(digestHex);
System.out.println("--------------------------------------");
}
else
System.out.println("VERIFICATION PASSED");
break;
default:
throw new IOException("Unknown certificate type " + subFilter);
};
};
};
};
public static void main(String[] args) throws Exception {
String fileName = "c:/test.pdf";
byte[] doc = Files.readAllBytes(Paths.get(fileName));
verifyPDF(doc);
}
}
Console output:
---------signatureContent length------------
18944
---------signedContent length------------
91250
---------isValid - checked by signerInformation ------------
true
---------certSerialNumber dec------------
8811972559309533840
---------certSerialNumber hex------------
0x7A4A6A5AD8227290
---------getContentType------------
1.2.840.113549.1.7.1
---------contentDigest base64------------
hcDVAjmJolBuurj1d2/2vWgO1bajqj1M8gGsQTGa/7w=
---------contentDigest hex------------
85C0D5023989A2506EBAB8F5776FF6BD680ED5B6A3AA3D4CF201AC41319AFFBC
---------digestAlgOID------------
2.16.840.1.101.3.4.2.1
org.bouncycastle.asn1.x509.AlgorithmIdentifier@da3a9fbd
---------encryptionAlgOID------------
1.2.840.113549.1.1.11
---------getSignature (encrypted) base64------------
uV9h778EUQ0wl7O9vNd5bvuzaq/XEx0zHeSnGKWAQQGqPe1YkKByZ1Pexo4ZZ6MqrKx7Ofpvje2gMhls6SAqqs4U2bMdrrM7a3udLDWLjjCHNy90zne2KUz/737gpIbiV4kzWbxlh44oWYgwM1Zc73hwWfh+I7G/fw0H//U4fgjnxbkXIEYU/zBOqQX4xlsWSAvAs1LB1N2+ySCGU9XvT5Btj9/F+e6hH8yMoyOFB1GrChdyasToUNq+5yAQa28nIxCcURvPo20mDtACgccLCVX+joMlFT21SI7mXIiFsIdBzGMqenyi7atJV53Gtvmp+tIpxowsvWTbCEMofsYVHw==
---------getSignature (encrypted) hex------------
B95F61EFBF04510D3097B3BDBCD7796EFBB36AAFD7131D331DE4A718A5804101AA3DED5890A0726753DEC68E1967A32AACAC7B39FA6F8DEDA032196CE9202AAACE14D9B31DAEB33B6B7B9D2C358B8E3087372F74CE77B6294CFFEF7EE0A486E257893359BC65878E2859883033565CEF787059F87E23B1BF7F0D07FFF5387E08E7C5B917204614FF304EA905F8C65B16480BC0B352C1D4DDBEC9208653D5EF4F906D8FDFC5F9EEA11FCC8CA323850751AB0A17726AC4E850DABEE720106B6F2723109C511BCFA36D260ED00281C70B0955FE8E8325153DB5488EE65C8885B08741CC632A7A7CA2EDAB49579DC6B6F9A9FAD229C68C2CBD64DB0843287EC6151F
---------publicKey base64------------
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxR7C91bL7cfR6NFMUvDCTtg4Fp91AH89efm7+1u2NO/nJNAtKvlhItNz4GDv28Ffp5WvKRZlHe5ORbnFvbHOaAAFG43k/tIx23ePa63TdYo8LY5M1/Auw2qDt7UZPZ4vbyABF7wtuNySrYOEcK5pdyAcXjECYHfEQSAH7DItOxMtU9I+Scl7oVo5rFHWEWWxBj3sIW5hxTUAVI9kZufi53XO5jEsHwh+5olR0tsJvrEeUPFHanPiBLi3Y+rrRMimPEeqfHV8jY+rZouovWEoivxNyLb3aooihAS5x4tC1A9p92TeQHBu3RZNfoC9KBB5F0iyb/CAFw9k5Aype8vgDQIDAQAB
---------publicKey hex------------
30820122300D06092A864886F70D01010105000382010F003082010A0282010100C51EC2F756CBEDC7D1E8D14C52F0C24ED838169F75007F3D79F9BBFB5BB634EFE724D02D2AF96122D373E060EFDBC15FA795AF2916651DEE4E45B9C5BDB1CE6800051B8DE4FED231DB778F6BADD3758A3C2D8E4CD7F02EC36A83B7B5193D9E2F6F200117BC2DB8DC92AD838470AE6977201C5E31026077C4412007EC322D3B132D53D23E49C97BA15A39AC51D61165B1063DEC216E61C53500548F6466E7E2E775CEE6312C1F087EE68951D2DB09BEB11E50F1476A73E204B8B763EAEB44C8A63C47AA7C757C8D8FAB668BA8BD61288AFC4DC8B6F76A8A228404B9C78B42D40F69F764DE40706EDD164D7E80BD2810791748B26FF080170F64E40CA97BCBE00D0203010001
---------getSignature (decrypted) base64------------
MDEwDQYJYIZIAWUDBAIBBQAEIOFF1ONvV7F2h9Cq5WOGXtM0CvzuwMyL2zahkGwcjebN
---------getSignature (decrypted) hex------------
3031300D060960864801650304020105000420E145D4E36F57B17687D0AAE563865ED3340AFCEEC0CC8BDB36A1906C1C8DE6CD
---------getAlgorithmId------------
2.16.840.1.101.3.4.2.1
---------getDigest hex------------
E145D4E36F57B17687D0AAE563865ED3340AFCEEC0CC8BDB36A1906C1C8DE6CD
---------signature.verify------------
false
---------signature bc.verify------------
false
--------------------------------------
---------VERIFICATION FAILED---------
---------calculated digest------------
85C0D5023989A2506EBAB8F5776FF6BD680ED5B6A3AA3D4CF201AC41319AFFBC
---------decrypted digest ------------
E145D4E36F57B17687D0AAE563865ED3340AFCEEC0CC8BDB36A1906C1C8DE6CD
--------------------------------------
SHA256
hcDVAjmJolBuurj1d2/2vWgO1bajqj1M8gGsQTGa/7w=
true
true
DSS Demonstration WebApp calculates the same digest as presented code.
Digest from signature can be decrypted using openssl:
signature=uV9h778EUQ0wl7O9vNd5bvuzaq/XEx0zHeSnGKWAQQGqPe1YkKByZ1Pexo4ZZ6MqrKx7Ofpvje2gMhls6SAqqs4U2bMdrrM7a3udLDWLjjCHNy90zne2KUz/737gpIbiV4kzWbxlh44oWYgwM1Zc73hwWfh+I7G/fw0H//U4fgjnxbkXIEYU/zBOqQX4xlsWSAvAs1LB1N2+ySCGU9XvT5Btj9/F+e6hH8yMoyOFB1GrChdyasToUNq+5yAQa28nIxCcURvPo20mDtACgccLCVX+joMlFT21SI7mXIiFsIdBzGMqenyi7atJV53Gtvmp+tIpxowsvWTbCEMofsYVHw==
publicKey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxR7C91bL7cfR6NFMUvDCTtg4Fp91AH89efm7+1u2NO/nJNAtKvlhItNz4GDv28Ffp5WvKRZlHe5ORbnFvbHOaAAFG43k/tIx23ePa63TdYo8LY5M1/Auw2qDt7UZPZ4vbyABF7wtuNySrYOEcK5pdyAcXjECYHfEQSAH7DItOxMtU9I+Scl7oVo5rFHWEWWxBj3sIW5hxTUAVI9kZufi53XO5jEsHwh+5olR0tsJvrEeUPFHanPiBLi3Y+rrRMimPEeqfHV8jY+rZouovWEoivxNyLb3aooihAS5x4tC1A9p92TeQHBu3RZNfoC9KBB5F0iyb/CAFw9k5Aype8vgDQIDAQAB
echo $signature | base64 --decode > signature.bin
echo -----BEGIN PUBLIC KEY----- > publicKey.pem
echo $publicKey >> publicKey.pem
echo -----END PUBLIC KEY----- >> publicKey.pem
openssl rsautl -verify -inkey publicKey.pem -pubin -in signature.bin |
openssl asn1parse -inform DER
0:d=0 hl=2 l= 49 cons: SEQUENCE
2:d=1 hl=2 l= 13 cons: SEQUENCE
4:d=2 hl=2 l= 9 prim: OBJECT :sha256
15:d=2 hl=2 l= 0 prim: NULL
17:d=1 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:E145D4E36F57B17687D0AAE563865ED3340AFCEEC0CC8BDB36A1906C1C8DE6CD
openssl also returns the same decrypted digest as presented code.
Message digest decrypted from signature (“real digest”) is definitely digest that must be used for comparison with calculated message digest. This value is in file; more precisely, encrypted in signature field and has to be decrypted with public key (extracted from certificate). This is a fact.
According to the SignerInformation documentation getContentDigest() - return the content digest that was calculated during verification. Verification was successful, as signerInformation verify returned true. As stated, this value is calculated, not read from file (or some field in file), and indeed it has to be calculated. These two values should match.
Those facts lead to the conclusion that both values are most likely equal but given hex or b64 representation differ. The question is how original binary of values is encoded and how are they decoded from binary to their representation or better the question is how to get “real digest” from pdf file using standard java libraries.