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
<pre lang="java">
/**
* 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
<pre lang="java">
/**
* 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);
}
}
Comments:
VB -
Hi! Thank you for this article. I’m trying to implement a “remote sign” with java and BC and this will help a lot. I’ve got a couple of question : 1) how can I implement getSignerInfoDigest() ? Is “digest.getContentDigest()” equivalent ? 2) how can I generate the detached signature WITHOUT the document (the client will generate only an hash and send it to the server, the server will sign it and return to the client, the client will you your code to join data with detached signature)