/*
 * Filename: IBMSignatureVerifier.java
 *
 *========================================================================
 * Modifications history
 *========================================================================
 * Last change: Date:       $Date: 2006/08/10 09:57:41 $
 *              By:         $Author: yhe $
 *              Revision:   $Revision: 1.1 $
 *========================================================================
 */
package com.imtf.atlas.sphinx2;

import com.ibm.xml.dsig.KeyInfo;
import com.ibm.xml.dsig.SignatureContext;
import com.ibm.xml.dsig.Validity;
import com.ibm.xml.dsig.util.AdHocIDResolver;

import org.apache.xpath.XPathAPI;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import java.security.Key;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

/**
 * IBM Signature verifier.
 * 
 * @author Yvan Hess (yvan.hess@imtf.ch)
 */
public class IBMSignatureVerifier
{
   /**
    * Get a XML document as a DOM document.
    *
    * @param xmlStream XML as an inputstream.
    *
    * @return The XML as as DOM document.
    *
    * @throws Exception thrown when an error occured during parsing.
    */
   static public Document getEDocumentAsDOM(InputStream xmlStream)
      throws Exception
   {
      DocumentBuilderFactory factory;
      DocumentBuilder parser;
      Document dom;

      // Parse the edocument and create a XML as DOM document 
      try
      {
         // Get document builder factory and set its properties
         factory = DocumentBuilderFactory.newInstance();
         factory.setAttribute("http://xml.org/sax/features/namespaces", new Boolean(true));

         // Get document parser and set its error handler
         parser = factory.newDocumentBuilder();

         // Parse the input stream 
         dom = parser.parse(xmlStream);
      }
      catch (Exception e)
      {
         throw new Exception("XML document cannot be converted as DOM document", e);
      }

      // Return the XML as a DOM document
      return dom;
   }

   /**
    * Get the signature element of a signed XML document.
    *
    * @param xmlDocument xml document.
    * @param signatureLocation signature location.
    *
    * @return xml signature element.
    *
    * @throws Exception thrown when an error occured.
    */
   static public Element getSingleSignatureElement(Document xmlDocument, String signatureLocation)
      throws Exception
   {
      Element ctx = xmlDocument.createElement("namespaceContext");

      ctx.setAttribute("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#");

      // Retrieve the signature element      
      Element sigElement;
      try
      {
         sigElement = (Element) XPathAPI.selectSingleNode(xmlDocument, signatureLocation, ctx);
      }

      catch (Exception e)
      {
         String msg = "Signature element can not be retrieved from XML document using Xpath expresion : [" + signatureLocation + "].";
         throw new Exception(msg, e);
      }

      return sigElement;
   }

   /**
    * Validate an XML document  using XSS4J toolkit.
    *
    * @param filename XML filename.
    * @param baseURI base URI of the document or null.
    *
    * @return true signature if valid, otherwise false.
    *
    * @throws Exception thrown when an error occured.
    */
   static public boolean verifyEDOC2WithIBMToolkit(String filename, String baseURI)
      throws Exception
   {
      ////////////////////////////////////
      // Get the EDOC as a DOM document
      ////////////////////////////////////
      InputStream edocStream = new FileInputStream(filename);
      Document doc = getEDocumentAsDOM(edocStream);

      //////////////////////////////////////
      // Search for a Signature element
      //////////////////////////////////////
      Element signature = getSingleSignatureElement(doc, "//ds:Signature");

      SignatureContext sigContext = new SignatureContext();

      if (baseURI != null)
      {
         sigContext.setEntityResolver(new URNResolver(baseURI));
      }
      sigContext.setIDResolver(new AdHocIDResolver(doc));

      ///////////////////////////////////////////////////
      // Get the public key used to sign the document
      ///////////////////////////////////////////////////
      Element keyInfoElement = KeyInfo.searchForKeyInfo(signature);
      Key key = null;
      if (keyInfoElement != null)
      {
         KeyInfo keyInfo = new KeyInfo(keyInfoElement);
         key = keyInfo.getX509Data()[0].getCertificates()[0].getPublicKey();
      }
      else
      {
         throw new Exception("Could not find the public key into the signed document.");
      }

      /////////////////////////
      // Check the validity 
      /////////////////////////
      Validity validity = sigContext.verify(signature, key);

      for (int i = 0; i < validity.getNumberOfReferences(); i++)
      {
         System.out.print("[" + i + "] ");
         System.out.print("\"" + validity.getReferenceURI(i) + "\"\t");
         if (validity.getReferenceValidity(i))
         {
            System.out.println("OK");
         }
         else
         {
            System.out.println("NG: " + validity.getReferenceMessage(i));
         }
      }

      if (!validity.getCoreValidity())
      {
         return false;
      }
      else
      {
         return true;
      }
   }

   /**
    * Validate an XML document  using XSS4J toolkit.
    *
    * @param filename XML filename.
    *
    * @return true signature if valid, otherwise false.
    *
    * @throws Exception thrown when an error occured.
    */
   static public boolean verifyEDOC2WithIBMToolkit(String filename)
      throws Exception
   {
      return verifyEDOC2WithIBMToolkit(filename, null);
   }
}

/**
 * URN resolver.
 */
class URNResolver implements org.xml.sax.EntityResolver
{
   private String baseURI;

   /**
    * Creates a new URNResolver object.
    *
    * @param baseURI URI of the document.
    */
   public URNResolver(String baseURI)
   {
      this.baseURI = baseURI;
   }

   /**
    * Return the Namespace Specific String of a urn.
    *
    * @param urn The urn.
    *
    * @return the Namespace Specific String of a urn or null if not an URN.
    */
   protected String getNSS(String urn)
   {
      int sepPosition = urn.indexOf(':', 4);

      if (sepPosition == -1)
      {
         return null;
      }

      return urn.substring(sepPosition + 1);
   }

   /**
    * Resolve external entities.
    *
    * @param systemID The public identifier of the external entity being referenced, or null if none was supplied.
    * @param uri The system identifier of the external entity being referenced.
    *
    * @return An InputSource object describing the new input source, or null to request that the parser open a regular URI connection to the system
    *         identifier.
    *
    * @throws SAXException thrown when an error occured.
    * @throws IOException thrown when an error occured.
    */
   public InputSource resolveEntity(String systemID, String uri)
      throws SAXException, IOException
   {
      String nss = getNSS(uri);
      String filenameURL = baseURI + "/" + nss;

      InputSource source = new org.xml.sax.InputSource(new FileInputStream(new File(filenameURL)));
      return source;
   }
}