Reputation: 2022
We need to send a hash / digest of an XML file to a remote signing service.
The signing service returns a PKCS#7 response. This includes the signature and the short-lived-x509 certificate that was used.
Question: what is the easiest solution to apply the information from the PKCS#7 to the XML file so that it is correctly signed? I am looking for an example (Plain Java or Apache Santuario).
Update:
This is the code used for signing (xml-dsig) an XML with Apache Santuario given a local keystore:
package test.signer.signer;
import org.apache.commons.io.IOUtils;
import org.apache.xml.security.Init;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.utils.Constants;
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.Key;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
/**
* from: https://stackoverflow.com/a/15911581/5658642
*/
public class CreateSignature
{
private static final String PRIVATE_KEY_ALIAS = "sgw-sign-client-keystore";
private static final String PRIVATE_KEY_PASS = "password";
private static final String KEY_STORE_PASS = "";
private static final String KEY_STORE_TYPE = "JKS";
public static void main(String... unused) throws Exception
{
final InputStream fileInputStream = Files.newInputStream(Paths.get("unsigned_DEV.xml"));
try
{
output(signFile(fileInputStream, new File("sgw-sign-client-keystore.jks")), "signed-test.xml");
} finally
{
IOUtils.closeQuietly(fileInputStream);
}
}
public static ByteArrayOutputStream signFile(InputStream xmlFile, File privateKeyFile) throws Exception
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(xmlFile);
Init.init();
final KeyStore keyStore = loadKeyStore(privateKeyFile);
final XMLSignature sig = new XMLSignature(doc, null, XMLSignature.ALGO_ID_SIGNATURE_RSA);
doc.getDocumentElement().appendChild(sig.getElement());
final Transforms transforms = new Transforms(doc);
transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
sig.addDocument("", transforms, Constants.ALGO_ID_DIGEST_SHA1);
// TODO replace with external signature
final Key privateKey = keyStore.getKey(PRIVATE_KEY_ALIAS, PRIVATE_KEY_PASS.toCharArray());
final X509Certificate cert = (X509Certificate) keyStore.getCertificate(PRIVATE_KEY_ALIAS);
sig.addKeyInfo(cert);
sig.addKeyInfo(cert.getPublicKey());
sig.sign(privateKey);
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_WITH_COMMENTS).canonicalizeSubtree(doc));
return outputStream;
}
private static KeyStore loadKeyStore(File privateKeyFile) throws Exception
{
final InputStream fileInputStream = Files.newInputStream(privateKeyFile.toPath());
try
{
final KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE);
keyStore.load(fileInputStream, null);
return keyStore;
} finally
{
IOUtils.closeQuietly(fileInputStream);
}
}
private static void output(ByteArrayOutputStream signedOutputStream, String fileName) throws IOException
{
final OutputStream fileOutputStream = Files.newOutputStream(Paths.get(fileName));
try
{
fileOutputStream.write(signedOutputStream.toByteArray());
fileOutputStream.flush();
} finally
{
IOUtils.closeQuietly(fileOutputStream);
}
}
}
This works so far. I now must replace the local keystore with a "remote keystore". The remote signing service takes a digest / hash and signs it. The response is a valid PKCS#7, which contains the certificate and the signature.
I am able to extract both with the following code (based on CMSSignedData from BouncyCastle):
String hashB64 = Base64.getEncoder().encodeToString(digest);
byte[] pkcs7Signature = remoteServiceClientService.sign(hashB64);
CMSSignedData cms = null;
try
{
cms = new CMSSignedData(pkcs7Signature);
SignerInformationStore signers = cms.getSignerInfos();
Collection<SignerInformation> c = signers.getSigners();
for (SignerInformation signer : c) {
// this collection will contain the signer certificate, if present
Collection<X509CertificateHolder> signerCol = cms.getCertificates().getMatches(signer.getSID());
SIG_CERT = new JcaX509CertificateConverter().getCertificate(signerCol.stream().findFirst().get());
}
List<byte[]> signatures = cms.getSignerInfos().getSigners().stream().map(SignerInformation::getSignature)
.collect(Collectors.toList());
byte[] pkcs1Signature = signatures.get(0);
SOPLogger.log("Plain Signature", pkcs1Signature);
return pkcs1Signature;
} catch (CMSException e)
{
throw new RuntimeException(e);
} catch (CertificateException e)
{
throw new RuntimeException(e);
}
I have no idea how to apply the extracted information instead of using a local keystore and are happy to try any hints.
Upvotes: 0
Views: 425