Attach payload into detached pkcs#7 signature

If you are doing signature generation via a hardware token (for instance 3Skey) then, for large files it is impractical to send the file to the hardware token. Instead you send a hash (SHA256), get a detached PKCS#7 signature and you need to re-attach the payload in java code. For once this was easier to do with plain JCE code instead of my favorite BouncyCastle provider. However for really large files BC does provide the streaming mechanism required.

Of course the best commands to use to help debug the code bellow are:

Verify pkcs#7 signature

#the -noverify means do not verify the certificate chain, this will only verify the signature not the originating certificate
openssl smime -inform DER -verify -noverify -in signature.p7s

Show the structure of the file (applies to all DER files)

#for debuging
openssl asn1parse -inform DER -i -in signature.p7s

Plain JCE

/**
 * Takes a detached pkcs#7 signature, and a content streams and generates an attached pkcs#7 signature
 * Does not verify if the hash of the content stream matches the signature
 * @param detachedP7sStream
 * @param content
 * @param attachedP7sStream
 */
public static void jceRepack(InputStream detachedP7sStream, InputStream contentStream, OutputStream attachedP7sStream) throws Exception {
	byte[] pkcs7Detached = readStream(detachedP7sStream);
 
	if (pkcs7Detached == null || pkcs7Detached.length == 0) {
		throw new Exception("PKCS7 Detached object is corrupted");
	}
 
	//parse pkcs#7 detached signature
	DerInputStream derMsg = new DerInputStream(pkcs7Detached);
	PKCS7 pkcs7 = new PKCS7(derMsg);
 
	//extracts information needed to reconstruct the pkcs#7 message
	AlgorithmId[] digestAlgorithmIds = pkcs7.getDigestAlgorithmIds();
	X509Certificate[] certificates = pkcs7.getCertificates();
	SignerInfo[] signerInfos = pkcs7.getSignerInfos();
 
	//data to be added
	byte[] data = readStream(contentStream);
 
	if (data == null || data.length == 0) {
		throw new Exception("There is not data content to verify");
	}
 
	//this is a in-memory operation, can generate an OOM for large files
	//see https://github.com/frohoff/jdk8u-jdk/blob/master/src/share/classes/sun/security/pkcs/ContentInfo.java
	ContentInfo contentInfo = new ContentInfo(data);
	PKCS7 pkcs7Full = new PKCS7(digestAlgorithmIds, contentInfo, certificates, signerInfos);
 
	try {
		pkcs7Full.encodeSignedData(attachedP7sStream);
		attachedP7sStream.close();
	}finally {
		try {
			attachedP7sStream.close();
		}catch (IOException e) { /*ignore*/ }
	}            
}

BouncyCastle with streaming

/**
 * Takes a detached pkcs#7 signature, and a content streams and generates an attached pkcs#7 signature
 * Verifies if the hash of the content stream matches the signature
 * @param detachedP7sStream
 * @param content
 * @param attachedP7sStream
 */
public static void repack(InputStream detachedP7sStream, InputStream content, OutputStream atachedP7sStream) throws Exception{
	Security.addProvider(new BouncyCastleProvider());
	try {
		CMSSignedData dettachedSignature = new CMSSignedData(detachedP7sStream);
 
		if(!dettachedSignature.isDetachedSignature()){
			throw new Exception("pkcs#7 signature already has a content");
		}
 
		SignerInformationStore signers = dettachedSignature.getSignerInfos();
		Store certStore = dettachedSignature.getCertificates();
 
		//only one signer supported
		Iterator i = signers.getSigners().iterator();
		if(!i.hasNext()){
			throw new Exception("No signer found");
		}
		//only one signer supported
		SignerInformation signer = (SignerInformation)i.next();
 
		//the signature bytes (DER encoded?)
		final byte[] signature = signer.getSignature();
 
		//very very important, the original signedAttributes, there is a difference between the 3skey lib and bc
		//which do not have the same list of attributes
		//3skey = 1.2.840.113549.1.9.4 (message digest), 1.2.840.113549.1.9.3 (content type), 1.2.840.113549.1.9.5 (signing time)}
		//bc has an additional 1.2.840.113549.1.9.52 (http://oidref.com/1.2.840.113549.1.9.52), 
		//see https://github.com/bcgit/bc-java/blob/6de1c17dda8ffdb19431ffcadbce1836867a27a9/pkix/src/main/java/org/bouncycastle/cms/DefaultSignedAttributeTableGenerator.java#L62 
		//thus the generated signatures DO NOT MATCH
		//remember signature = signature(data + signed attributes)
		//I lost 1 day because I did not knew that!
		AttributeTable signedAttributes = signer.getSignedAttributes();
 
		//there is a hash which is stored in the message-digest attribute sometimes
		byte[] signerDigest = getSignerInfoDigest(signer);
		//log.info("hash: " + Hex.toHexString(signerDigest));
		//printSignedAttributes(signer);
 
		//the certificates (a collection of one) from the original pkcs#7
		Collection certCollection = certStore.getMatches(signer.getSID());
		X509CertificateHolder signCertificate = (X509CertificateHolder)certCollection.iterator().next();
 
		//fake contentSigner, no signature is actually done, the original signature is returned
        ContentSigner nonSigner = new ContentSigner() {
            @Override
            public byte[] getSignature() {
                return signature;
            }
 
            @Override
            public OutputStream getOutputStream() {
                return new ByteArrayOutputStream();
            }
 
            @Override
            public AlgorithmIdentifier getAlgorithmIdentifier() {
            	//if you need to support other algorithm it can be found in signer.getDigestAlgorithmID and signer.getEncryptionAlgorithmID
                return new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");
            }
        };
 
        CMSSignedDataStreamGenerator signatureGenerator = new CMSSignedDataStreamGenerator();
		//in memory: CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
 
        //store original certificates
		signatureGenerator.addCertificates(certStore);
 
		JcaSignerInfoGeneratorBuilder signerInfoBuilder = new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build());
 
		//set the signedAttributes table VERY IMPORTANT!
		signerInfoBuilder.setSignedAttributeGenerator(new SimpleAttributeTableGenerator(signedAttributes));
        signatureGenerator.addSignerInfoGenerator(signerInfoBuilder.build(nonSigner, signCertificate));
 
        //2nd param true means "attached" signature (includePayload or enveloped also used as terms)
        OutputStream signatureStream = signatureGenerator.open(atachedP7sStream, true);
 
        MessageDigest digest = MessageDigest.getInstance("SHA256");
        DigestInputStream digestInputStream = new DigestInputStream(content, digest);
 
        //write content
		byte[] b = new byte[1024];
		int noOfBytes = 0;
		while ((noOfBytes = digestInputStream.read(b)) != -1) {
			signatureStream.write(b, 0, noOfBytes);
		}
		signatureStream.close();
 
		//verify digest of content versus the stored one
		byte[] calculatedContentDigest = digest.digest();
		if(!ByteUtils.equals(calculatedContentDigest, signerDigest)){
			throw new Exception("Calculated digest for content [" + Hex.toHexString(calculatedContentDigest) + 
					"] does not match signerInfo digest [" + Hex.toHexString(signerDigest) + "]");
 
		}else{
			log.info("Repack: calculated digest matches, same content: " + Hex.toHexString(calculatedContentDigest));
		}
 
		/* for in memory generation the following code can be used: 
		ByteArrayOutputStream data = new ByteArrayOutputStream();
        byte[] b = new byte[1024];
		int noOfBytes = 0;
 
		while ((noOfBytes = content.read(b)) != -1) {
			data.write(b, 0, noOfBytes);
		}
 
        CMSSignedData sigData = gen.generate(new CMSProcessableByteArray(data.toByteArray()), true);
		attachedP7sStream.write(sigData.getEncoded());
		attachedP7sStream.close();
		*/
	} catch (Exception e) {
		log.error("Repack failed", e);
		throw new Exception(e);
	}		
}

Leave a Reply

*