beat
beat

Reputation: 2022

XML Signature remote

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

Answers (0)

Related Questions