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); } } |
Recent Comments