/**
* WS-Attacker - A Modular Web Services Penetration Testing Framework Copyright
* (C) 2013 Christian Mainka
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package wsattacker.library.signatureWrapping.util;
import java.io.*;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.logging.*;
import javax.xml.XMLConstants;
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.crypto.dsig.spec.XPathFilter2ParameterSpec;
import javax.xml.crypto.dsig.spec.XPathType;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPathExpressionException;
import org.apache.log4j.Logger;
import org.apache.ws.security.message.token.Timestamp;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import wsattacker.library.xmlutilities.dom.DomUtilities;
import static wsattacker.library.xmlutilities.dom.DomUtilities.domToString;
import static wsattacker.library.xmlutilities.dom.DomUtilities.evaluateXPath;
import static wsattacker.library.xmlutilities.dom.DomUtilities.findChildren;
import static wsattacker.library.xmlutilities.dom.DomUtilities.getFirstChildElement;
import static wsattacker.library.xmlutilities.namespace.NamespaceConstants.PREFIX_NS_DS;
import static wsattacker.library.xmlutilities.namespace.NamespaceConstants.PREFIX_NS_WSSE;
import static wsattacker.library.xmlutilities.namespace.NamespaceConstants.URI_NS_DS;
import static wsattacker.library.xmlutilities.namespace.NamespaceConstants.URI_NS_WSSE_1_0;
import static wsattacker.library.xmlutilities.namespace.NamespaceConstants.URI_NS_WSU;
import wsattacker.library.xmlutilities.namespace.NamespaceResolver;
/**
* Main Signature Class Is able to sign and verify a Document by XPath and ID
*/
public class Signer
{
private static final Logger LOG = Logger.getLogger( Signer.class );
private KeyInfoInterface keyInfo;
final static String idString = ""; // whole document
final static int TIMESTAMP = 60 * 15; // 15min
// public static String c14n_method =
// CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS;
public static String c14n_method = CanonicalizationMethod.EXCLUSIVE;
public String printBytes( byte[] bytes )
{
StringBuffer buffer = new StringBuffer();
for ( byte b : bytes )
{
buffer.append( String.format( "%2X ", b ) );
}
return buffer.toString();
}
public Signer( KeyInfoInterface keyInfo )
{
this.keyInfo = keyInfo;
}
public KeyInfoInterface getKeyInfo()
{
return keyInfo;
}
public void addTimestamp( Element parent )
{
Timestamp timestamp = new Timestamp( false, parent.getOwnerDocument(), TIMESTAMP );
parent.appendChild( timestamp.getElement() );
}
// http://www.java2s.com/Code/Java/JDK-6/SignSOAPmessage.htm
// http://java.sun.com/developer/technicalArticles/xml/dig_signature_api/
public void sign( Document doc, List<String> whatToSign )
throws Exception
{
// Create a DOM XMLSignatureFactory that will be used to
// generate the enveloped signature.
XMLSignatureFactory fac = XMLSignatureFactory.getInstance();
// DigestMethod
DigestMethod digestMethod = fac.newDigestMethod( DigestMethod.SHA1, null );
// Instantiate the document to be signed.
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature( XMLConstants.FEATURE_SECURE_PROCESSING, true );
// Of course, we need namespaces awereness
dbf.setNamespaceAware( true );
Element envelope = getFirstChildElement( doc );
Element header = getFirstChildElement( envelope );
// Element body = getNextSiblingElement(header); // not needed, just
// nice to know
// create WS Security Header
Element wsseHeader;
List<Element> wsseHeaders = findChildren( header, "Security", URI_NS_WSSE_1_0 );
if ( wsseHeaders.isEmpty() )
{
wsseHeader = doc.createElementNS( URI_NS_WSSE_1_0, PREFIX_NS_WSSE + ":Security" );
header.appendChild( wsseHeader );
}
else
{
wsseHeader = wsseHeaders.get( 0 );
}
List<String> xpathListToSign = new ArrayList<String>();
List<String> idListToSign = new ArrayList<String>();
// seperate ids and xpaths
for ( String r : whatToSign )
{
if ( r.startsWith( "#" ) )
{
idListToSign.add( r );
}
else
{
// check if xpaths match any elements
List<Element> match = (List<Element>) evaluateXPath( doc, r );
if ( match.size() < 1 )
{
throw new Exception( "Invalid document, can't find node by XPATH:\n\n" + r + "\n\n"
+ domToString( doc ) );
}
xpathListToSign.add( r );
}
}
if ( xpathListToSign.isEmpty() && idListToSign.isEmpty() )
{
throw new Exception( "Nothing to sign specified" );
}
// create Reference
List<Reference> allRefs = new ArrayList<Reference>();
if ( !xpathListToSign.isEmpty() )
{
List<XPathType> types = new ArrayList<XPathType>( 1 );
// Example: How to use XPath
// types.add(new XPathType(" //ToBeSigned ",
// XPathType.Filter.INTERSECT));
// types.add(new XPathType(" //NotToBeSigned ",
// XPathType.Filter.SUBTRACT));
// types.add(new XPathType(" //ReallyToBeSigned ",
// XPathType.Filter.UNION));
// First XPATH must INTERSECT
NamespaceResolver res = new NamespaceResolver( doc );
types.add( new XPathType( xpathListToSign.get( 0 ), XPathType.Filter.INTERSECT, res.getPrefixUriMap() ) );
// Other XPaths UNION
for ( int i = 1; i < xpathListToSign.size(); ++i )
{
types.add( new XPathType( xpathListToSign.get( i ), XPathType.Filter.UNION, res.getPrefixUriMap() ) );
}
XPathFilter2ParameterSpec xp = new XPathFilter2ParameterSpec( types );
Transform transform = fac.newTransform( Transform.XPATH2, xp );
List<Transform> transformList = Collections.singletonList( transform );
Reference ref = fac.newReference( idString, digestMethod, transformList, null, null );
allRefs.add( ref );
}
// Add Ids
for ( String id : idListToSign )
{
// Reference ref = fac.newReference("first"+id, digestMethod, null,
// null, "last"+id);
List<Attr> matches = DomUtilities.findAttributeByValue( doc, id.substring( 1 ) );
List<Transform> transformList = null;
if ( !matches.isEmpty() )
{
Element signedElement = matches.get( 0 ).getOwnerElement();
Node tmp = wsseHeader;
do
{
if ( tmp.isEqualNode( signedElement ) )
{
Transform transform = fac.newTransform( Transform.ENVELOPED, (TransformParameterSpec) null );
transformList = Collections.singletonList( transform );
}
tmp = tmp.getParentNode();
}
while ( tmp != null && tmp.getNodeType() == Node.ELEMENT_NODE );
}
Reference ref = fac.newReference( id, digestMethod, transformList, null, null );
allRefs.add( ref );
}
// Create the SignedInfo
CanonicalizationMethod canonicalizationMethod =
fac.newCanonicalizationMethod( c14n_method, (C14NMethodParameterSpec) null );
SignatureMethod signatureMethod = fac.newSignatureMethod( SignatureMethod.RSA_SHA1, null );
SignedInfo si = fac.newSignedInfo( canonicalizationMethod, signatureMethod, allRefs );
// Load the KeyStore and get the signing key and certificate.
KeyStore ks = KeyStore.getInstance( "JKS" );
FileInputStream keyfileInputStream;
keyfileInputStream = new FileInputStream( keyInfo.getKeyStoreFileName() );
ks.load( keyfileInputStream, keyInfo.getKeyStorePassword().toCharArray() );
KeyStore.PrivateKeyEntry keyEntry =
(KeyStore.PrivateKeyEntry) ks.getEntry( keyInfo.getEntityName(),
new KeyStore.PasswordProtection(
keyInfo.getEntityPassword().toCharArray() ) );
X509Certificate cert = (X509Certificate) keyEntry.getCertificate();
// Create the KeyInfo containing the X509Data.
KeyInfoFactory kif = fac.getKeyInfoFactory();
List<Object> x509Content = new ArrayList<Object>();
x509Content.add( cert.getSubjectX500Principal().getName() );
x509Content.add( cert );
X509Data xd = kif.newX509Data( x509Content );
KeyInfo ki = kif.newKeyInfo( Collections.singletonList( xd ) );
// Append id to first body child (as an example)
// Element firstBodyChild = getFirstChildElement(body);
// Attr id = doc.createAttributeNS(uriWSU, prefixWSU+":Id");
// id.setValue(idString);
// firstBodyChild.setAttributeNodeNS(id);
// Create a DOMSignContext and specify the RSA PrivateKey and
// location of the resulting XMLSignature's parent element.
DOMSignContext dsc = new DOMSignContext( keyEntry.getPrivateKey(), wsseHeader );
// dsc.setDefaultNamespacePrefix("ds");
dsc.putNamespacePrefix( URI_NS_DS, PREFIX_NS_DS );
// Fix due to Java 7 update, URIDereferencer does no longer work
// dsc.setURIDereferencer(dsc.getURIDereferencer());
markWsuIdAttributes( doc );
// for (String id : idListToSign) {
// Element signed = DomUtilities.findElementByWsuId(doc,
// id.substring(1)).get(0);
// signed.setIdAttributeNS(URI_NS_WSU, "Id", true);
// }
// Create the XMLSignature, but don't sign it yet.
XMLSignature xmlSignature = fac.newXMLSignature( si, ki );
// Marshal, generate, and sign the enveloped signature.
// doc.normalizeDocument();
xmlSignature.sign( dsc );
// doc.normalizeDocument(); // necessary?
}
public boolean verifyTimestamp( Document doc )
throws Exception
{
LOG.debug( "Verifying Timestamp of Document:\n" + domToString( doc ) );
Element envelope = doc.getDocumentElement();
List<Element> headerList = findChildren( envelope, "Header", envelope.getNamespaceURI() );
if ( headerList.size() != 1 )
{
LOG.warn( "Could not find SOAP Header" );
return false;
}
Element header = headerList.get( 0 );
List<Element> securityList = findChildren( header, "Security", URI_NS_WSSE_1_0 );
if ( securityList.size() != 1 )
{
LOG.warn( "Could not find WS Security Header" );
return false;
}
Element security = securityList.get( 0 );
// Validate Timestamp
List<Element> timestampList = findChildren( security, "Timestamp", URI_NS_WSU );
if ( timestampList.size() != 1 )
{
LOG.warn( "There are " + timestampList.size() + " Timestamp Elemenets" );
return false;
}
return verifyTimestamp( timestampList.get( 0 ) );
}
public boolean verifyTimestamp( Element timestampElement )
throws Exception
{
Timestamp timestamp = new Timestamp( timestampElement );
boolean isExpired = timestamp.isExpired();
if ( isExpired )
{
LOG.warn( "Timestamp is expired" );
}
return isExpired;
}
// http://download.oracle.com/docs/cd/E17802_01/webservices/webservices/docs/1.6/tutorial/doc/XMLDigitalSignatureAPI8.html
public boolean verifySignature( Document doc )
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException,
MarshalException, XMLSignatureException
{
// doc.normalize();
// doc.normalizeDocument();
// LOG.debug("Verifying Document:\n" + domToString(doc));
Element envelope = doc.getDocumentElement();
List<Element> headerList = findChildren( envelope, "Header", envelope.getNamespaceURI() );
if ( headerList.size() != 1 )
{
LOG.warn( "Could not find SOAP Header" );
return false;
}
Element header = headerList.get( 0 );
List<Element> securityList = findChildren( header, "Security", URI_NS_WSSE_1_0 );
if ( securityList.size() != 1 )
{
LOG.warn( "Found " + securityList.size() + " WS Security Headers" );
return false;
}
Element security = securityList.get( 0 );
List<Element> signatureList = findChildren( security, "Signature", XMLSignature.XMLNS );
if ( signatureList.size() != 1 )
{
LOG.warn( "There are " + signatureList.size() + " Signature Elements" );
return false;
}
Element signature = signatureList.get( 0 );
// keypair
DOMValidateContext valContext = new DOMValidateContext( new KeyValueKeySelector(), signature );
// valContext.setURIDereferencer(valContext.getURIDereferencer());
markWsuIdAttributes( doc );
XMLSignatureFactory fac = XMLSignatureFactory.getInstance();
XMLSignature xmlSignature = fac.unmarshalXMLSignature( valContext );
boolean allvalid = xmlSignature.validate( valContext );
if ( !allvalid )
{
@SuppressWarnings( "unchecked" )
Iterator<Reference> itRef = xmlSignature.getSignedInfo().getReferences().iterator();
for ( int j = 0; itRef.hasNext(); j++ )
{ // j is only used if we
// calculate the Reference
// Digest, which is
// out-commmented for
// performance issues...
Reference r = itRef.next();
// These Lines are usefull for debugging, but take
// performance...
// **************************************************************
boolean refValid = r.validate( valContext );
LOG.debug( "ref[" + j + "] validity status: " + refValid );
LOG.debug( printBytes( r.getCalculatedDigestValue() ) + "(Calculated)" );
LOG.debug( printBytes( r.getDigestValue() ) + "(Saved value)" );
LOG.debug( "Type: " + r.getType() + " / URI: " + r.getURI() );
}
// These Lines are usefull for debugging, but take performance...
// **************************************************************
boolean sv = xmlSignature.getSignatureValue().validate( valContext );
LOG.info( "signature validation status: " + sv );
}
// Policy is OK, now validating
LOG.info( " ==> Signature is " + ( allvalid ? "VALID" : "invalid" ) );
return xmlSignature.validate( valContext );
}
private void markWsuIdAttributes( Document doc )
{
final String xpath =
"//attribute::*[local-name()='Id' and namespace-uri()='" + URI_NS_WSU + "']/parent::node()";
try
{
List<Element> matchList = (List<Element>) DomUtilities.evaluateXPath( doc, xpath );
for ( Element match : matchList )
{
match.setIdAttributeNS( URI_NS_WSU, "Id", true );
}
}
catch ( XPathExpressionException ex )
{
java.util.logging.Logger.getLogger( Signer.class.getName() ).log( Level.SEVERE, null, ex );
}
}
}