You are viewing a plain text version of this content. The canonical link for it is here.
Posted to fx-dev@ws.apache.org by Soumadeep Sen <So...@webMethods.com> on 2006/11/23 08:24:13 UTC

RE: [jira] Created: (WSS-59) Crypto.properties doesn't allow enco ded password for keystore - WSS4J1.5

Hi Abhijit 

You are right, I completely agree with you. Whether to use a Symmetric
Encryption Algorithm or not is again an issue as there are disadvantages
with it too.

But for a basic first level security I feel we can have a base64 encoded
password. An appropriate solution would be to make it extendable so that
others can select an encryption mechanism depending on their requirement.

The other thing is that we also need to understand how other applications
would be affected by this change. Such as AXIS.

Thanks
Soumadeep

-----Original Message-----
From: Abhijit Sharma [mailto:asharma@amberpoint.com] 
Sent: Thursday, November 23, 2006 12:42 PM
To: 'Soumadeep (JIRA)'; wss4j-dev@ws.apache.org
Subject: RE: [jira] Created: (WSS-59) Crypto.properties doesn't allow
encoded password for keystore - WSS4J1.5

Hi Soumyadeep,

I was just wondering what kind of enhanced security is provided by base64
encoding the password in the Crypto.properties. Anyone can base64 decode the
password and get to the real password. I suppose the only security it can
offer is visual security i.e. someone looking over your shoulder cannot
easily remember the password.

I think some Symmetric Encryption Algorithm (like AES etc) should be used
for encrypting the password written to the file to make it really secure. 

Regards,
Abhijit

-----Original Message-----
From: Soumadeep (JIRA) [mailto:jira@apache.org]
Sent: Wednesday, November 22, 2006 10:38 PM
To: wss4j-dev@ws.apache.org
Subject: [jira] Created: (WSS-59) Crypto.properties doesn't allow encoded
password for keystore - WSS4J1.5

Crypto.properties doesn't allow encoded password for keystore - WSS4J1.5
------------------------------------------------------------------------

                 Key: WSS-59
                 URL: http://issues.apache.org/jira/browse/WSS-59
             Project: WSS4J
          Issue Type: New Feature
         Environment: ALL
            Reporter: Soumadeep
         Assigned To: Davanum Srinivas
            Priority: Blocker


Crypto.properties file doesn't allow encoded passwords for keystore and
alias. The plain text password would be unacceptable for most of the
enterprises.

The load method of AbstractCrypto.java file actually picks up the passwords
from the crypto.properties file. I have made the required changes and am
putting it below. This only accounts for the keystore password as the alias
password would be usage specific. Please check the two files
crypto.properties and AbstractCrypto.java. The files are from the wss4j main
trunk - svn

Thanks
Soumadeep 


Crypto.properties
=============

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.cry
pto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
// This property has been added
org.apache.ws.security.crypto.merlin.keystore.base64.encoded=false

org.apache.ws.security.crypto.merlin.keystore.password=soumadeep
org.apache.ws.security.crypto.merlin.keystore.alias=soumadeep
org.apache.ws.security.crypto.merlin.alias.password=soumadeep
org.apache.ws.security.crypto.merlin.file=resources/soumadeep.jks

AbstractCrypto.java
===============
/*
 * Copyright  2003-2004 The Apache Software Foundation.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package org.apache.ws.security.components.crypto;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.util.Base64;
import org.apache.ws.security.util.Loader;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;

/**
 * Created by IntelliJ IDEA.
 * User: dims
 * Date: Sep 15, 2005
 * Time: 9:50:40 AM
 * To change this template use File | Settings | File Templates.
 */
public abstract class AbstractCrypto implements Crypto {
    private static Log log = LogFactory.getLog(AbstractCrypto.class);
    protected static CertificateFactory certFact;
    protected Properties properties = null;
    protected KeyStore keystore = null;
    static String SKI_OID = "2.5.29.14";

    /**
     * Constructor
     *
     * @param properties
     */
    public AbstractCrypto(Properties properties) throws CredentialException,
IOException {
    	this(properties,AbstractCrypto.class.getClassLoader());
    }

    /**
     * This allows providing a custom class loader to load the resources,
etc
     * @param properties
     * @param loader
     * @throws CredentialException
     * @throws IOException
     */
    public AbstractCrypto(Properties properties, ClassLoader loader) throws
CredentialException, IOException {
        /*
        * if no properties .. just return an instance, the rest will be
        * done later or this instance is just used to handle certificate
        * conversions in this implementatio
        */
        if (properties == null) {
            return;
        }
        this.properties = properties;
        String location =
this.properties.getProperty("org.apache.ws.security.crypto.merlin.file");


		InputStream is = null;

		java.net.URL url = Loader.getResource(loader, location);

		if(url != null) {

			is =  url.openStream();

		} else {

			is = new java.io.FileInputStream(location);

		}


        /**
         * If we don't find it, then look on the file system.
         */
        if (is == null) {
            try {
                is = new FileInputStream(location);
            } catch (Exception e) {
                throw new CredentialException(3, "proxyNotFound", new
Object[]{location});
            }
        }

        /**
         * Load the keystore
         */
        try {
            load(is);
        } finally {
            is.close();
        }
    }

    
    /**
     * Singleton certificate factory for this Crypto instance.
     * <p/>
     *
     * @return Returns a <code>CertificateFactory</code> to construct
     *         X509 certficates
     * @throws org.apache.ws.security.WSSecurityException
     *
     */
    public synchronized CertificateFactory getCertificateFactory() throws
WSSecurityException {
        if (certFact == null) {
            try {
                String provider =
properties.getProperty("org.apache.ws.security.crypto.merlin.cert.provider")
;
                if (provider == null || provider.length() == 0) {
                    certFact = CertificateFactory.getInstance("X.509");
                } else {
                    certFact = CertificateFactory.getInstance("X.509",
provider);
                }
            } catch (CertificateException e) {
                throw new
WSSecurityException(WSSecurityException.SECURITY_TOKEN_UNAVAILABLE,
                        "unsupportedCertType");
            } catch (NoSuchProviderException e) {
                throw new
WSSecurityException(WSSecurityException.SECURITY_TOKEN_UNAVAILABLE,
                        "noSecProvider");
            }
        }
        return certFact;
    }

    /**
     * load a X509Certificate from the input stream.
     * <p/>
     *
     * @param in The <code>InputStream</code> array containg the X509 data
     * @return Returns a X509 certificate
     * @throws org.apache.ws.security.WSSecurityException
     *
     */
    public X509Certificate loadCertificate(InputStream in) throws
WSSecurityException {
        X509Certificate cert = null;
        try {
            cert =
                    (X509Certificate)
getCertificateFactory().generateCertificate(in);
        } catch (CertificateException e) {
            throw new
WSSecurityException(WSSecurityException.SECURITY_TOKEN_UNAVAILABLE,
                    "parseError");
        }
        return cert;
    }

    /**
     * Gets the private key identified by <code>alias</> and
<code>password</code>.
     * <p/>
     *
     * @param alias    The alias (<code>KeyStore</code>) of the key owner
     * @param password The password needed to access the private key
     * @return The private key
     * @throws Exception
     */
    public PrivateKey getPrivateKey(String alias, String password) throws
Exception {
        if (alias == null) {
            throw new Exception("alias is null");
        }
        boolean b = keystore.isKeyEntry(alias);
        if (!b) {
            log.error("Cannot find key for alias: " + alias);
            throw new Exception("Cannot find key for alias: " + alias);
        }
        Key keyTmp = keystore.getKey(alias, password.toCharArray());
        if (!(keyTmp instanceof PrivateKey)) {
            throw new Exception("Key is not a private key, alias: " +
alias);
        }
        return (PrivateKey) keyTmp;
    }

    private Vector splitAndTrim(String inString) {
        X509NameTokenizer nmTokens = new X509NameTokenizer(inString);
        Vector vr = new Vector();

        while (nmTokens.hasMoreTokens()) {
            vr.add(nmTokens.nextToken());
        }
        java.util.Collections.sort(vr);
        return vr;
    }

    /**
     * Lookup a X509 Certificate in the keystore according to a given
     * the issuer of a Certficate.
     * <p/>
     * The search gets all alias names of the keystore and gets the
certificate chain
     * for each alias. Then the Issuer fo each certificate of the chain
     * is compared with the parameters.
     *
     * @param issuer The issuer's name for the certificate
     * @return alias name of the certificate that matches the issuer name
     *         or null if no such certificate was found.
     */
    public String getAliasForX509Cert(String issuer)
            throws WSSecurityException {
        return getAliasForX509Cert(issuer, null, false);
    }

    /**
     * Lookup a X509 Certificate in the keystore according to a given serial
number and
     * the issuer of a Certficate.
     * <p/>
     * The search gets all alias names of the keystore and gets the
certificate chain
     * for each alias. Then the SerialNumber and Issuer fo each certificate
of the chain
     * is compared with the parameters.
     *
     * @param issuer       The issuer's name for the certificate
     * @param serialNumber The serial number of the certificate from the
named issuer
     * @return alias name of the certificate that matches serialNumber and
issuer name
     *         or null if no such certificate was found.
     */
    public String getAliasForX509Cert(String issuer, BigInteger
serialNumber)
            throws WSSecurityException {
        return getAliasForX509Cert(issuer, serialNumber, true);
    }

    /*
    * need to check if "getCertificateChain" also finds certificates that
are
    * used for enryption only, i.e. they may not be signed by a CA
    * Otherwise we must define a restriction how to use certificate:
    * each certificate must be signed by a CA or is a self signed
Certificate
    * (this should work as well).
    * --- remains to be tested in several ways --
    */
    private String getAliasForX509Cert(String issuer, BigInteger
serialNumber,
                                       boolean useSerialNumber)
            throws WSSecurityException {
        Vector issuerRDN = splitAndTrim(issuer);
        X509Certificate x509cert = null;
        Vector certRDN = null;
        Certificate cert = null;

        try {
            for (Enumeration e = keystore.aliases(); e.hasMoreElements();) {
                String alias = (String) e.nextElement();
                Certificate[] certs = keystore.getCertificateChain(alias);
                if (certs == null || certs.length == 0) {
                    // no cert chain, so lets check if getCertificate gives
us a  result.
                    cert = keystore.getCertificate(alias);
                    if (cert == null) {
                        return null;
                    }
                } else {
                    cert = certs[0];
                }
                if (!(cert instanceof X509Certificate)) {
                    continue;
                }
                x509cert = (X509Certificate) cert;
                if (!useSerialNumber ||
                        useSerialNumber &&
x509cert.getSerialNumber().compareTo(serialNumber) == 0) {
                    certRDN =
splitAndTrim(x509cert.getIssuerDN().getName());
                    if (certRDN.equals(issuerRDN)) {
                        return alias;
                    }
                }
            }
        } catch (KeyStoreException e) {
            throw new WSSecurityException(WSSecurityException.FAILURE,
                    "keystore");
        }
        return null;
    }

    /**
     * Lookup a X509 Certificate in the keystore according to a given
     * SubjectKeyIdentifier.
     * <p/>
     * The search gets all alias names of the keystore and gets the
certificate chain
     * or certificate for each alias. Then the SKI for each user certificate
     * is compared with the SKI parameter.
     *
     * @param skiBytes The SKI info bytes
     * @return alias name of the certificate that matches serialNumber and
issuer name
     *         or null if no such certificate was found.
     * @throws org.apache.ws.security.WSSecurityException
     *          if problems during keystore handling or wrong certificate
(no SKI data)
     */

    public String getAliasForX509Cert(byte[] skiBytes) throws
WSSecurityException {
        Certificate cert = null;
        boolean found = false;

        try {
            for (Enumeration e = keystore.aliases(); e.hasMoreElements();) {
                String alias = (String) e.nextElement();
                Certificate[] certs = keystore.getCertificateChain(alias);
                if (certs == null || certs.length == 0) {
                    // no cert chain, so lets check if getCertificate gives
us a  result.
                    cert = keystore.getCertificate(alias);
                    if (cert == null) {
                        return null;
                    }
                } else {
                    cert = certs[0];
                }
                if (!(cert instanceof X509Certificate)) {
                    continue;
                }
                byte[] data = getSKIBytesFromCert((X509Certificate) cert);
                if (data.length != skiBytes.length) {
                    continue;
                }
                if (Arrays.equals(data, skiBytes)) {
                    return alias;
                }
            }
        } catch (KeyStoreException e) {
            throw new WSSecurityException(WSSecurityException.FAILURE,
                    "keystore");
        }
        return null;
    }

    /**
     * Return a X509 Certificate alias in the keystore according to a given
Certificate
     * <p/>
     *
     * @param cert The certificate to lookup
     * @return alias name of the certificate that matches the given
certificate
     *         or null if no such certificate was found.
     */

/*
     * See comment above
     */
    public String getAliasForX509Cert(Certificate cert) throws
WSSecurityException {
        try {
            String alias = keystore.getCertificateAlias(cert);
            if (alias != null)
                return alias;
            // Use brute force search
            Enumeration e = keystore.aliases();
            while (e.hasMoreElements()) {
                alias = (String) e.nextElement();
                X509Certificate cert2 = (X509Certificate)
keystore.getCertificate(alias);
                if (cert2.equals(cert)) {
                    return alias;
                }
            }
        } catch (KeyStoreException e) {
            throw new WSSecurityException(WSSecurityException.FAILURE,
                    "keystore");
        }
        return null;
    }

    /**
     * Retrieves the alias name of the default certificate which has been
     * specified as a property. This should be the certificate that is used
for
     * signature and encryption. This alias corresponds to the certificate
that
     * should be used whenever KeyInfo is not poresent in a signed or
     * an encrypted message. May return null.
     *
     * @return alias name of the default X509 certificate
     */
    public String getDefaultX509Alias() {
        if (properties == null) {
            return null;
        }
        return
properties.getProperty("org.apache.ws.security.crypto.merlin.keystore.alias"
);
    }

    /**
     * Gets the list of certificates for a given alias.
     * <p/>
     *
     * @param alias Lookup certificate chain for this alias
     * @return Array of X509 certificates for this alias name, or
     *         null if this alias does not exist in the keystore
     */
    public X509Certificate[] getCertificates(String alias) throws
WSSecurityException {
        Certificate[] certs = null;
        try {
            certs = keystore.getCertificateChain(alias);
            if (certs == null || certs.length == 0) {
                // no cert chain, so lets check if getCertificate gives us a
result.
                Certificate cert = keystore.getCertificate(alias);
                if (cert == null) {
                    return null;
                }
                certs = new Certificate[]{cert};
            }
        } catch (KeyStoreException e) {
            throw new WSSecurityException(WSSecurityException.FAILURE,
                    "keystore");
        }
        X509Certificate[] x509certs = new X509Certificate[certs.length];
        for (int i = 0; i < certs.length; i++) {
            x509certs[i] = (X509Certificate) certs[i];
        }
        return x509certs;
    }

    /**
     * Lookup a X509 Certificate in the keystore according to a given
     * Thumbprint.
     * <p/>
     * The search gets all alias names of the keystore, then reads the
certificate chain
     * or certificate for each alias. Then the thumbprint for each user
certificate
     * is compared with the thumbprint parameter.
     *
     * @param thumb The SHA1 thumbprint info bytes
     * @return alias name of the certificate that matches the thumbprint
     *         or null if no such certificate was found.
     * @throws org.apache.ws.security.WSSecurityException
     *          if problems during keystore handling or wrong certificate
     */

    public String getAliasForX509CertThumb(byte[] thumb) throws
WSSecurityException {
        Certificate cert = null;
        MessageDigest sha = null;

        try {
            sha = MessageDigest.getInstance("SHA-1");
        } catch (NoSuchAlgorithmException e1) {
            throw new WSSecurityException(
                    0,
                    "noSHA1availabe");
        }
        try {
            for (Enumeration e = keystore.aliases(); e.hasMoreElements();) {
                String alias = (String) e.nextElement();
                Certificate[] certs = keystore.getCertificateChain(alias);
                if (certs == null || certs.length == 0) {
                    // no cert chain, so lets check if getCertificate gives
us a  result.
                    cert = keystore.getCertificate(alias);
                    if (cert == null) {
                        return null;
                    }
                } else {
                    cert = certs[0];
                }
                if (!(cert instanceof X509Certificate)) {
                    continue;
                }
                sha.reset();
                try {
                    sha.update(cert.getEncoded());
                } catch (CertificateEncodingException e1) {
                    throw new WSSecurityException(
                            WSSecurityException.SECURITY_TOKEN_UNAVAILABLE,
                            "encodeError");
                }
                byte[] data = sha.digest();

                if (Arrays.equals(data, thumb)) {
                    return alias;
                }
            }
        } catch (KeyStoreException e) {
            throw new WSSecurityException(WSSecurityException.FAILURE,
                    "keystore");
        }
        return null;
    }

    /**
     * A Hook for subclasses to set the keystore without having to
     * load it from an <code>InputStream</code>.
     *
     * @param ks existing keystore
     */
    public void setKeyStore(KeyStore ks) {
        keystore = ks;
    }

    /**
     * Loads the the keystore from an <code>InputStream </code>.
     * <p/>
     *
     * @param input <code>InputStream</code> to read from
     * @throws CredentialException
     */
    public void load(InputStream input) throws CredentialException {
        if (input == null) {
            throw new IllegalArgumentException("input stream cannot be
null");
        }
        try {
            String provider =
properties.getProperty("org.apache.ws.security.crypto.merlin.keystore.provid
er");
            if (provider == null || provider.length() == 0) {
                keystore = KeyStore.getInstance
 
(properties.getProperty("org.apache.ws.security.crypto.merlin.keystore.type"
,
                                KeyStore.getDefaultType()));
            } else {
                keystore = KeyStore.getInstance
 
(properties.getProperty("org.apache.ws.security.crypto.merlin.keystore.type"
,
                                KeyStore.getDefaultType()), provider);
            }
			String password = null;
			String base64Encoded =
properties.getProperty("org.apache.ws.security.crypto.merlin.keystore.base64
.encoded","false");
			password =
properties.getProperty("org.apache.ws.security.crypto.merlin.keystore.passwo
rd",
                        "security");
			if(base64Encoded.equalsIgnoreCase("true")){
            	password = new String(Base64.decode(password));
            }
			
            keystore.load(input, (password == null || password.length() ==
0) ? new char[0] : password.toCharArray());
        } catch (IOException e) {
            e.printStackTrace();
            throw new CredentialException(3, "ioError00", e);
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
            throw new CredentialException(3, "secError00", e);
        } catch (Exception e) {
            e.printStackTrace();
            throw new CredentialException(-1, "error00", e);
        }
    }

    /**
     * Reads the SubjectKeyIdentifier information from the certificate.
     * <p/>
     * If the the certificate does not contain a SKI extension then
     * try to compute the SKI according to RFC3280 using the
     * SHA-1 hash value of the public key. The second method described
     * in RFC3280 is not support. Also only RSA public keys are supported.
     * If we cannot compute the SKI throw a WSSecurityException.
     *
     * @param cert The certificate to read SKI
     * @return The byte array conating the binary SKI data
     */
    public byte[] getSKIBytesFromCert(X509Certificate cert)
            throws WSSecurityException {
        /*
           * Gets the DER-encoded OCTET string for the extension value
(extnValue)
           * identified by the passed-in oid String. The oid string is
represented
           * by a set of positive whole numbers separated by periods.
           */
        byte[] derEncodedValue = cert.getExtensionValue(SKI_OID);

        if (cert.getVersion() < 3 || derEncodedValue == null) {
            PublicKey key = cert.getPublicKey();
            if (!(key instanceof RSAPublicKey)) {
                throw new WSSecurityException(
                        1,
                        "noSKIHandling",
                        new Object[]{"Support for RSA key only"});
            }
            byte[] encoded = key.getEncoded();
            // remove 22-byte algorithm ID and header
            byte[] value = new byte[encoded.length - 22];
            System.arraycopy(encoded, 22, value, 0, value.length);
            MessageDigest sha;
            try {
                sha = MessageDigest.getInstance("SHA-1");
            } catch (NoSuchAlgorithmException ex) {
                throw new WSSecurityException(
                        1,
                        "noSKIHandling",
                        new Object[]{"Wrong certificate version (<3) and no
SHA1 message digest availabe"});
            }
            sha.reset();
            sha.update(value);
            return sha.digest();
        }

        /**
         * Strip away first four bytes from the DerValue (tag and length of
         * ExtensionValue OCTET STRING and KeyIdentifier OCTET STRING)
         */
        byte abyte0[] = new byte[derEncodedValue.length - 4];

        System.arraycopy(derEncodedValue, 4, abyte0, 0, abyte0.length);
        return abyte0;
    }

    public KeyStore getKeyStore() {
        return this.keystore;
    }

    /**
     * Lookup X509 Certificates in the keystore according to a given DN of
the subject of the certificate
     * <p/>
     * The search gets all alias names of the keystore and gets the
certificate (chain)
     * for each alias. Then the DN of the certificate is compared with the
parameters.
     *
     * @param subjectDN The DN of subject to look for in the keystore
     * @return Vector with all alias of certificates with the same DN as
given in the parameters
     * @throws org.apache.ws.security.WSSecurityException
     *
     */
    public String[] getAliasesForDN(String subjectDN) throws
WSSecurityException {

        // Store the aliases found
        Vector aliases = new Vector();

        Certificate cert = null;

        // The DN to search the keystore for
        Vector subjectRDN = splitAndTrim(subjectDN);

        // Look at every certificate in the keystore
        try {
            for (Enumeration e = keystore.aliases(); e.hasMoreElements();) {
                String alias = (String) e.nextElement();

                Certificate[] certs = keystore.getCertificateChain(alias);
                if (certs == null || certs.length == 0) {
                    // no cert chain, so lets check if getCertificate gives
us a  result.
                    cert = keystore.getCertificate(alias);
                    if (cert == null) {
                        return null;
                    }
                    certs = new Certificate[]{cert};
                } else {
                    cert = certs[0];
                }
                if (cert instanceof X509Certificate) {
                    Vector foundRDN = splitAndTrim(((X509Certificate)
cert).getSubjectDN().getName());

                    if (subjectRDN.equals(foundRDN)) {
                        aliases.add(alias);
                    }
                }
            }
        } catch (KeyStoreException e) {
            throw new WSSecurityException(WSSecurityException.FAILURE,
                    "keystore");
        }

        // Convert the vector into an array
        String[] result = new String[aliases.size()];
        for (int i = 0; i < aliases.size(); i++)
            result[i] = (String) aliases.elementAt(i);

        return result;
    }
}




--
This message is automatically generated by JIRA.
-
If you think it was sent incorrectly contact one of the administrators:
http://issues.apache.org/jira/secure/Administrators.jspa
-
For more information on JIRA, see: http://www.atlassian.com/software/jira

        

---------------------------------------------------------------------
To unsubscribe, e-mail: wss4j-dev-unsubscribe@ws.apache.org
For additional commands, e-mail: wss4j-dev-help@ws.apache.org




---------------------------------------------------------------------
To unsubscribe, e-mail: wss4j-dev-unsubscribe@ws.apache.org
For additional commands, e-mail: wss4j-dev-help@ws.apache.org

---------------------------------------------------------------------
To unsubscribe, e-mail: wss4j-dev-unsubscribe@ws.apache.org
For additional commands, e-mail: wss4j-dev-help@ws.apache.org