0

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.

beat
  • 1,857
  • 1
  • 22
  • 36
  • 1
    Please show your attempts you have tried and the problems/error messages you get from your attempts. It is unclear what you are asking or what the problem is. Are you talking about [xmldsig](https://www.w3.org/TR/xmldsig-core1/)? – Progman Feb 04 '23 at 19:31
  • Yes, xml-dsig is to be used. What I have done so far: I am able to sign and verify a given xml file with a _local_ keystore. I am able to extract the signature and the certificate from the supplied pkcs#7 (CMS) response. I have no idea how to replace the local keystore and supply instead the extracted signature and x509 certificate. – beat Feb 05 '23 at 13:15
  • 1
    Please [edit] your question to include the source code you have or what you have tried so far. Also include the sample data on how your XML file is or should be signed from your local keystore and from the external service. If possible provide a [mcve]. Also include the response you get from the external service and include your desired result of your finished XML file. – Progman Feb 05 '23 at 13:29
  • Read over https://www.bouncycastle.org/docs/pkixdocs1.5on/org/bouncycastle/cert/jcajce/JcaX509CertificateHolder.html and https://www.bouncycastle.org/docs/pkixdocs1.5on/org/bouncycastle/operator/ContentVerifierProvider.html and see if throwing those in with your logic you can get to other pieces you're missing put together. You did a good job writing all that code, so play with those a little incorporating accordingly for my "hint" to you as you mentioned. @beat one more here https://www.bouncycastle.org/docs/pkixdocs1.8on/org/bouncycastle/operator/jcajce/JcaContentVerifierProviderBuilder.html – Bitcoin Murderous Maniac Feb 11 '23 at 01:14

0 Answers0