diff --git a/jsign-core/src/main/java/net/jsign/pe/PEFile.java b/jsign-core/src/main/java/net/jsign/pe/PEFile.java index a48c73d4..24b7284b 100644 --- a/jsign-core/src/main/java/net/jsign/pe/PEFile.java +++ b/jsign-core/src/main/java/net/jsign/pe/PEFile.java @@ -16,34 +16,34 @@ package net.jsign.pe; -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; +import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.StandardOpenOption; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; import net.jsign.DigestAlgorithm; import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; -import org.bouncycastle.asn1.ASN1Encodable; -import org.bouncycastle.asn1.cms.Attribute; -import org.bouncycastle.asn1.cms.AttributeTable; -import org.bouncycastle.asn1.cms.ContentInfo; +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.cms.*; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.DigestInfo; import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cms.CMSProcessable; import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.DefaultCMSSignatureAlgorithmNameGenerator; import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.operator.DefaultAlgorithmNameFinder; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; /** * Portable Executable File. @@ -828,4 +828,253 @@ public synchronized void pad(int multiple) throws IOException { channel.position(channel.size()); channel.write(ByteBuffer.allocate((int) padding)); } + + /** + * A verification method for verifying signed signatures + * + * Apart from the authenticode_pe.docx microsoft provides https://www.ietf.org/rfc/rfc2315.txt was used to + * implement this method + */ + public void verify() throws VerificationException, IOException { + + MessageDigest md; + List signatures = getSignatures(); + + if(!signatures.isEmpty()) { + + for(CMSSignedData signedData : signatures) { + + DigestAlgorithm signedDataAlgorithm = DigestAlgorithm.of(signedData.getDigestAlgorithmIDs().iterator().next().getAlgorithm()); + SignerInformation signerInformation; + + //get SpcIndirectContent structure from signed data, see pefile documentation + ContentInfo contentInfo = signedData.toASN1Structure(); + SignedData signedData1 = SignedData.getInstance(contentInfo.getContent()); + ContentInfo contentInfo1 = signedData1.getEncapContentInfo(); + + //Study ASN1Dump.dumpAsString() to implement getSpcIndirectDataContent(...) or any other ASN.1 parser + DigestInfo digestInfo = getSpcIndirectDataContent(contentInfo1); + DigestAlgorithm spcDigestAlgorithm = DigestAlgorithm.of((digestInfo.getAlgorithmId().getAlgorithm())); + + //#1 Check that the digest algorithms match + if(signedData.getDigestAlgorithmIDs().size() != 1) { + throw new VerificationException("Signed Data must contain exactly one DigestAlgorithm"); + } + + //Check that SpcIndirectContent DigestAlgorithm equals CMSSignedData algorithm + if(!(spcDigestAlgorithm.oid.getId()).equals(signedDataAlgorithm.oid.getId())) { + throw new VerificationException("Signed Data algorithm doesn't match with spcDigestAlgorithm"); + } + + //Check that SignerInfo DigestAlgorithm equals CMSSignedData algorithm + if(signedData.getSignerInfos().size() != 1) { + throw new VerificationException("Signed Data must contain exactly one SignerInfo"); + } + + signerInformation = signedData.getSignerInfos().iterator().next(); + if(!(signerInformation.getDigestAlgorithmID().getAlgorithm().getId()).equals(signedDataAlgorithm.oid.getId())) { + throw new VerificationException("Signed Data algorithm doesn't match with SignerInformation algorithm"); + } + + //#2 Check the embedded hash in spcIndirectContent matches with the computed hash of the pefile + if(!Arrays.equals(computeDigest(signedDataAlgorithm), digestInfo.getDigest())) + throw new VerificationException("The embedded hash in the SignedData is not equal to the computed hash"); + + //#3 The hash of the spc blob should be equal to message digest in authenticated attributes of signed data + //Get the message digest from authenticated attributes, see authenticode_pe documentation + byte messageDigestInAuthenticatedAttr[]; + Attribute attribute = (Attribute)signerInformation.getSignedAttributes().toHashtable().get(CMSAttributes.messageDigest); + Object digestObj = attribute.getAttrValues().iterator().next(); + + if(digestObj instanceof ASN1OctetString) { + + ASN1OctetString oct = (ASN1OctetString)digestObj; + + messageDigestInAuthenticatedAttr = oct.getOctets(); + } + else { + throw new VerificationException("No message digest was found in authenticated attributes"); + } + + //Get the spc blob + byte[] spcBlob = getSpcBlob(contentInfo1.getContent().toASN1Primitive()); + + //Now calculate the digest of the spcblob + try { + md = MessageDigest.getInstance(signedData.getDigestAlgorithmIDs().iterator().next().getAlgorithm().toString()); + } catch (NoSuchAlgorithmException e) { + throw new VerificationException(e.getMessage()); + } + + md.update(spcBlob); + byte[] spcDigest = md.digest(); + + //Compare both and throw exception if not equal + if(!Arrays.equals(messageDigestInAuthenticatedAttr, spcDigest)) + throw new VerificationException("The hash of stripped content of SpcInfo does not match digest found in authenticated attributes"); + + //#4 Check the hash in Authenticated Attributes with Encrypted Hash + X509CertificateHolder h = (X509CertificateHolder) signedData.getCertificates().getMatches(signerInformation.getSID()).iterator().next(); + JcaX509CertificateConverter converter = new JcaX509CertificateConverter(); + X509Certificate certificate = null; + try { + certificate = converter.getCertificate(h); + } catch (CertificateException e) { + throw new VerificationException(e.getMessage()); + } + PublicKey key = certificate.getPublicKey(); + Signature signature = null; + try { + SignerInfo signerInfo = signerInformation.toASN1Structure(); + String digestAndEncryptionAlgorithmName = new DefaultCMSSignatureAlgorithmNameGenerator().getSignatureName(signerInformation.getDigestAlgorithmID(), signerInfo.getDigestEncryptionAlgorithm()); + signature = Signature.getInstance(digestAndEncryptionAlgorithmName); + } catch (NoSuchAlgorithmException e) { + throw new VerificationException(e.getMessage()); + } + try { + signature.initVerify(key); + } catch (InvalidKeyException e) { + throw new VerificationException(e.getMessage()); + } + + try { + signature.update(signerInformation.getEncodedSignedAttributes()); + if(!signature.verify(signerInformation.getSignature())) { + throw new VerificationException("The hash in the the authenticated attributes doesn't match the encrypted hash(getSignature())"); + } + //Note that the getSignature() method returns the encrypted hash in the SignerInformation + } catch (SignatureException e) { + throw new VerificationException(e.getMessage()); + } + + //#4 Check the countersigner hash + if(signerInformation.getCounterSignatures().size() != 0) { + + SignerInformation counterSignerInformation = signerInformation.getCounterSignatures().iterator().next(); + try { + md = MessageDigest.getInstance(counterSignerInformation.getDigestAlgorithmID().getAlgorithm().toString()); + } catch (NoSuchAlgorithmException e) { + throw new VerificationException(e.getMessage()); + } + md.update(signerInformation.getSignature()); + byte[] authAttrHash = md.digest(); + + byte[] messageDigestInCounterSignature; + Attribute attributeOfCounterSignature = (Attribute)counterSignerInformation.getSignedAttributes().toHashtable().get(CMSAttributes.messageDigest); + Object digestObjOfCounterSignature = attributeOfCounterSignature.getAttrValues().iterator().next(); + + if(digestObjOfCounterSignature instanceof ASN1OctetString) { + + ASN1OctetString oct = (ASN1OctetString)digestObjOfCounterSignature; + + messageDigestInCounterSignature = oct.getOctets(); + } + else { + throw new VerificationException("No message digest was found in authenticated attributes of counter signature"); + } + + //Compare both and throw exception if not equal + if(!Arrays.equals(messageDigestInCounterSignature, authAttrHash)) + throw new VerificationException("The digest of encrypted hash in the signerInformation does not match with digest found in counter signature"); + + } + } + } + else { + + throw new VerificationException("No Signature Present"); + } + } + + /** + * This method returns the SpcIndirectDataContent data structure which is described in authenticode_pe.docx + * This structure consists of the digest of the contents of the EXE which was calculated at the time of signing + * This will be retrieved and compared to check if the digest calculated now matches with this digest + * That's how we verify the integrity of the EXE + * + * @param contentInfo + * @return + */ + private DigestInfo getSpcIndirectDataContent(ContentInfo contentInfo) { + + DigestInfo digestInfo; + + AlgorithmIdentifier algId = null; + byte[] digest = null; + + if((contentInfo.getContentType().getId()).equals(AuthenticodeObjectIdentifiers.SPC_INDIRECT_DATA_OBJID.getId())) { + + ASN1Primitive obj = contentInfo.getContent().toASN1Primitive(); + + if(obj instanceof ASN1Sequence) { + + Enumeration e = ((ASN1Sequence)obj).getObjects(); + + e.nextElement(); + Object messageDigestObj = e.nextElement(); + + if(messageDigestObj instanceof ASN1Sequence) { + + Enumeration e1 = ((ASN1Sequence)messageDigestObj).getObjects(); + + + Object seq = e1.nextElement(); + Object digestObj = e1.nextElement(); + + if(seq instanceof ASN1Sequence) { + + Enumeration e2 = ((ASN1Sequence)seq).getObjects(); + + Object digestAlgorithmObj = e2.nextElement(); + + if(digestAlgorithmObj instanceof ASN1ObjectIdentifier) { + + AlgorithmIdentifier a = new DefaultDigestAlgorithmIdentifierFinder().find(new DefaultAlgorithmNameFinder().getAlgorithmName((ASN1ObjectIdentifier) digestAlgorithmObj)); + algId = AlgorithmIdentifier.getInstance(a); + } + } + + if(digestObj instanceof ASN1OctetString) { + + ASN1OctetString oct = (ASN1OctetString)digestObj; + + digest = oct.getOctets(); + } + } + } + } + + digestInfo = new DigestInfo(algId, digest); + + return digestInfo; + } + + /** + * This method returns the constituents of the spcIndirectDataContent minus the some data + * Refer RFC2315, 9.3 to find what have been omitted and the remaining is returned as bytes + * This will be digested and compared to the digest in the AuthenticatedAttributes in the certificate + * + * @param primitive + * @return + * @throws IOException + */ + private byte[] getSpcBlob(ASN1Primitive primitive) throws IOException { + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + if(primitive instanceof ASN1Sequence) { + + Iterator it = ((ASN1Sequence) primitive).iterator(); + + while(it.hasNext()) { + ASN1Primitive p = (ASN1Primitive)it.next(); + outputStream.write(p.getEncoded()); + } + + return outputStream.toByteArray(); + } + + return null; + } } + diff --git a/jsign-core/src/main/java/net/jsign/pe/VerificationException.java b/jsign-core/src/main/java/net/jsign/pe/VerificationException.java new file mode 100644 index 00000000..884c9248 --- /dev/null +++ b/jsign-core/src/main/java/net/jsign/pe/VerificationException.java @@ -0,0 +1,12 @@ +package net.jsign.pe; + +public class VerificationException extends Exception { + + public VerificationException(String message) { + super(message); + } + + public VerificationException(String message, Throwable cause) { + super(message, cause); + } +}