de.schlichtherle.xml.GenericCertificate.java Source code

Java tutorial

Introduction

Here is the source code for de.schlichtherle.xml.GenericCertificate.java

Source

/*
 * GenericCertificate.java
 *
 * Created on 28. Januar 2005, 14:19
 */
/*
 * Copyright 2005 Schlichtherle IT Services
 *
 * 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 de.schlichtherle.xml;

import java.beans.*;
import java.io.*;
import java.lang.reflect.*;
import java.security.*;
import java.security.cert.*;
import java.util.*;

import org.apache.commons.codec.binary.Base64;

/**
 * This non-visual JavaBean implements authentic runtime objects whose
 * integrity cannot be compromised without being detected.
 * The idea and the design of this class is inspired by both
 * {@link java.security.SignedObject} and
 * {@link java.security.cert.Certificate}.
 * <p>
 * More specifically, a <tt>GenericCertificate</tt> contains an XML string
 * encoded representation of an arbitrary object in the "encoded"
 * property and a Base64 immutable string representation of the object's
 * corresponding digital signature in the "signature" property.
 * The selection of this representation form and the design of this class
 * as a plain JavaBean allows its instances to be serialized using either
 * this package's {@link PersistenceService}, JDK's
 * {@link java.beans.XMLEncoder}, or the vanilla {@link ObjectOutputStream}.
 * <p>
 * For an object to be successfully digitally signed, it must support
 * serialisation via JDK's XMLEncoder, for which this package
 * provides the class PersistenceService.
 * This easy-to-use class allows you to provide custom
 * {@link java.beans.PersistenceDelegate} instances for the serialisation of any
 * classes which do not implement the JavaBean design pattern and are not
 * supported by XMLEncoder as a default.
 * <p>
 * Whenever an instance of this GenericCertificate class is created,
 * you can arbitrarily set and get its "encoded" and "signature" properties,
 * allowing you to provide even custom deserialisation methods other than this
 * class already provides via the aforementioned classes. However, once this
 * instance is used to either sign or verify another object it gets locked,
 * allowing subsequent read access to its properties only.
 * <p>
 * The underlying signing algorithm is designated by the Signature
 * object passed to the <tt>sign</tt> and the <tt>verify</tt> methods.
 * <p>
 * A typical usage for signing is the following:
 * <code><pre>
 * GenericCertificate cert = new GenericCertificate();
 * Signature signingEngine = Signature.getInstance(algorithm,
 *                                                 provider);
 * try {
 *     cert.sign(myObject, signingKey, signingEngine);
 * }
 * catch (PropertyVetoException signingVetoed) {
 *     // ...
 * }
 * catch (PersistenceServiceException serialisationFailed) {
 *     // ...
 * }
 * catch (InvalidKeyException invalidKey) {
 *     // ...
 * }
 * catch (SignatureException signingEngineBroken) {
 *     // ...
 * }
 * </pre></code>
 * A typical usage for verification is the following (having
 * received GenericCertificate <code>cert</code>):
 * <code><pre>
 * Signature verificationEngine =
 *     Signature.getInstance(algorithm, provider);
 * try {
 *     cert.verify(publicKey, verificationEngine));
 * }
 * catch (PropertyVetoException verificationVetoed) {
 *     // ...
 * }
 * catch (InvalidKeyException invalidKey) {
 *     // ...
 * }
 * catch (SignatureException verificationEngineBroken) {
 *     // ...
 * }
 * catch (GenericCertificateException integrityCompromised) {
 *     // ...
 * }
 * Object myObject = cert.getContent();
 * </pre></code>
 * Several points are worth noting:
 * <ul><li>
 * There is no need to initialize the signing or verification engine,
 * as it will be re-initialized inside the {@link #sign} and {@link #verify}
 * methods. Secondly, for verification to succeed, the specified
 * public key must be the public key corresponding to the private key
 * used to sign the GenericCertificate.</li>
 * <li>
 * In contrast to SignedObject, this class adds more security
 * as it is impossible to retrieve the signed object without verifying
 * the signature before. A SignedObject however could
 * be deserialised from a compromised file and the application developer
 * may erraticaly forget to call the {@link java.security.SignedObject#verify}
 * method before retrieving the signed object by calling
 * {@link java.security.SignedObject#getObject()}.</li>
 * <li>
 * More importantly, for flexibility reasons, the
 * sign() and verify() methods allow for
 * customized signature engines, which can implement signature
 * algorithms that are not installed formally as part of a crypto
 * provider. However, it is crucial that the programmer writing the
 * verifier code be aware what {@link java.security.Signature} engine is being
 * used, as its own implementation of the {@link Signature#verify} method
 * is invoked to verify a signature. In other words, a malicious
 * Signature engine may choose to always return true on
 * verification in an attempt to bypass a security check.</li>
 * <li>
 * The signature algorithm can be, among others, the NIST standard
 * DSA, using DSA and SHA-1.  The algorithm is specified using the
 * same convention as that for signatures. The DSA algorithm using the
 * SHA-1 message digest algorithm can be specified, for example, as
 * "SHA/DSA" or "SHA-1/DSA" (they are equivalent).  In the case of
 * RSA, there are multiple choices for the message digest algorithm,
 * so the signing algorithm could be specified as, for example,
 * "MD2/RSA", "MD5/RSA" or "SHA-1/RSA". The algorithm name must be
 * specified, as there is no default.</li>
 * <li>
 * The name of the Cryptography Package Provider is designated
 * by the Signature parameter to the sign() and 
 * verify() methods. If the provider is not specified,
 * the default provider is used. Each installation can be configured
 * to use a particular provider as default.</li>
 * <li>The property change listeners are <em>not</em> persistet when
 * using {@link ObjectOutputStream} or {@link XMLEncoder}.</li>
 * <li>{@link Object#equals(Object)} and {@link Object#hashCode()} are
 * <em>not</em> overridden by this class because different JVMs will produce
 * different literal encodings of the same object and we cannot rely on a proper
 * <tt>equals(...)</tt> implementation in the class of a signed object.</li>
 * </ul>
 * Potential applications of GenericCertificate include: 
 * <ul><li>
 * It can be used internally to any Java runtime as an unforgeable
 * authorization token -- one that can be passed around without the
 * fear that the token can be maliciously modified without being
 * detected.</li>
 * <li>
 * It can be used to sign and serialize data/object for storage outside
 * the Java runtime (e.g., storing critical access control data on
 * disk).</li>
 * <li>
 * Nested GenericCertificates can be used to construct a logical
 * sequence of signatures, resembling a chain of authorization and
 * delegation.</li>
 * </ul>
 * <p>
 * This class is designed to be thread-safe.
 * 
 * @see java.security.Signature
 * @see java.security.SignedObject
 * @see java.security.cert.Certificate
 * 
 * @author Christian Schlichtherle
 */
public final class GenericCertificate implements Serializable, XMLConstants {

    private static final String BASE64_CHARSET = "US-ASCII"; // NOI18N
    private static final String SIGNATURE_ENCODING = "US-ASCII/Base64"; // NOI18N

    /**
     * Holds value of property locked - cannot be serialised!!!
     */
    private transient boolean locked;

    /**
     * Holds value of property encoded.
     */
    private String encoded;

    /**
     * Holds value of property signature.
     */
    private String signature;

    /**
     * Holds value of property signatureAlgorithm.
     */
    private String signatureAlgorithm;

    /**
     * Holds value of property signatureEncoding.
     */
    private String signatureEncoding;

    /**
     * Utility field used by bound properties.
     */
    private transient PropertyChangeSupport propertyChangeSupport;

    /**
     * Utility field used by constrained properties.
     */
    private transient VetoableChangeSupport vetoableChangeSupport;

    /** Creates a new generic certificate. */
    public GenericCertificate() {
    }

    /**
     * Copy constructor for the given generic certificate.
     * Note that the new certificate is unlocked and does not have any
     * event listeners.
     */
    public GenericCertificate(final GenericCertificate cert) {
        try {
            setEncoded(cert.getEncoded());
            setSignature(cert.getSignature());
            setSignatureAlgorithm(cert.getSignatureAlgorithm());
            setSignatureEncoding(cert.getSignatureEncoding());
        } catch (PropertyVetoException cannotHappen) {
            throw new AssertionError(cannotHappen);
        }
    }

    /**
     * Encodes and signs the given <tt>content</tt> in this certificate and
     * locks it.
     * <p>
     * Please note the following:
     * <ul>
     * <li>This method will throw a <tt>PropertyVetoException</tt> if this
     *     certificate is already locked, i.e. if it has been signed or
     *     verified before.</li>
     * <li>Because this method locks this certificate, a subsequent call to
     *     {@link #sign(Object, PrivateKey, Signature)} or
     *     {@link #verify(PublicKey, Signature)} is redundant
     *     and will throw a <tt>PropertyVetoException</tt>.
     *     Use {@link #isLocked()} to detect whether a
     *     generic certificate has been successfuly signed or verified before
     *     or call {@link #getContent()} and expect an 
     *     Exception to be thrown if it hasn't.</li>
     * <li>There is no way to unlock this certificate.
     *     Call the copy constructor of {@link GenericCertificate} if you
     *     need an unlocked copy of the certificate.</li>
     * </ul>
     * 
     * @param content The object to sign. This must either be a JavaBean or an
     *        instance of any other class which is supported by
     *        <tt>{@link PersistenceService}</tt>
     *        - maybe <tt>null</tt>.
     * @param signingKey The private key for signing
     *        - may <em>not</em> be <tt>null</tt>.
     * @param signingEngine The signature signing engine
     *        - may <em>not</em> be <tt>null</tt>.
     * 
     * @throws NullPointerException If the preconditions for the parameters
     *         do not hold.
     * @throws GenericCertificateIsLockedException If this certificate is
     *         already locked by signing or verifying it before.
     *         Note that this is actually a subclass of
     *         {@link PropertyVetoException}.
     * @throws PropertyVetoException If locking the certifificate (and thus
     *         signing the object) is vetoed by any listener.
     * @throws PersistenceServiceException If the object cannot be serialised.
     * @throws InvalidKeyException If the verification key is invalid.
     */
    public synchronized final void sign(final Object content, final PrivateKey signingKey,
            final Signature signingEngine) throws NullPointerException, GenericCertificateIsLockedException,
            PropertyVetoException, PersistenceServiceException, InvalidKeyException {
        // Check parameters.
        if (signingKey == null)
            throw new NullPointerException("signingKey");
        if (signingEngine == null)
            throw new NullPointerException("signingEngine");

        // Check lock status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "locked", Boolean.valueOf(isLocked()),
                Boolean.TRUE); // NOI18N
        if (isLocked())
            throw new GenericCertificateIsLockedException(evt);

        // Notify vetoable listeners and give them a chance to veto.
        fireVetoableChange(evt);

        try {
            // Encode the object.
            final byte[] beo = PersistenceService.store2ByteArray(content);

            // Sign the byte encoded object.
            signingEngine.initSign(signingKey);
            signingEngine.update(beo);
            final byte[] b64es = Base64.encodeBase64(signingEngine.sign()); // the base64 encoded signature
            final String signature = new String(b64es, 0, b64es.length, BASE64_CHARSET);

            // Store results.
            setEncoded(new String(beo, XML_CHARSET));
            setSignature(signature);
            setSignatureAlgorithm(signingEngine.getAlgorithm());
            setSignatureEncoding(SIGNATURE_ENCODING); // NOI18N
        } catch (UnsupportedEncodingException cannotHappen) {
            throw new AssertionError(cannotHappen);
        } catch (SignatureException cannotHappen) {
            throw new AssertionError(cannotHappen);
        }

        // Lock this certificate and notify property change listeners.
        this.locked = true;
        firePropertyChange(evt);
    }

    /**
     * 
     * Verifies the digital signature of the encoded content in this
     * certificate and locks it.
     * <p>
     * Please note the following:
     * <ul>
     * <li>This method will throw a <tt>PropertyVetoException</tt> if this
     *     certificate is already locked, i.e. if it has been signed or
     *     verified before.</li>
     * <li>Because this method locks this certificate, a subsequent call to
     *     {@link #sign(Object, PrivateKey, Signature)} or
     *     {@link #verify(PublicKey, Signature)} is redundant
     *     and will throw a <tt>PropertyVetoException</tt>.
     *     Use {@link #isLocked()} to detect whether a
     *     generic certificate has been successfuly signed or verified before
     *     or call {@link #getContent()} and expect an 
     *     Exception to be thrown if it hasn't.</li>
     * <li>There is no way to unlock this certificate.
     *     Call the copy constructor of {@link GenericCertificate} if you
     *     need an unlocked copy of the certificate.</li>
     * </ul>
     * 
     * @param verificationKey The public key for verification
     *        - may <em>not</em> be <tt>null</tt>.
     * @param verificationEngine The signature verification engine
     *        - may <em>not</em> be <tt>null</tt>.
     * 
     * @throws NullPointerException If the preconditions for the parameters
     *         do not hold.
     * @throws GenericCertificateIsLockedException If this certificate is
     *         already locked by signing or verifying it before.
     *         Note that this is actually a subclass of
     *         {@link PropertyVetoException}.
     * @throws PropertyVetoException If locking the certifificate (and thus
     *         verifying the object) is vetoed by any listener.
     * @throws InvalidKeyException If the verification key is invalid.
     * @throws SignatureException If signature verification failed.
     * @throws GenericCertificateIntegrityException If the integrity of this
     *         certificate has been compromised.
     */
    public synchronized final void verify(final PublicKey verificationKey, final Signature verificationEngine)
            throws NullPointerException, GenericCertificateIsLockedException, PropertyVetoException,
            InvalidKeyException, SignatureException, GenericCertificateIntegrityException {
        // Check parameters.
        if (verificationKey == null)
            throw new NullPointerException("verificationKey");
        if (verificationEngine == null)
            throw new NullPointerException("verificationEngine");

        // Check lock status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "locked", Boolean.valueOf(isLocked()),
                Boolean.TRUE); // NOI18N
        if (isLocked())
            throw new GenericCertificateIsLockedException(evt);

        // Notify vetoable listeners and give them a chance to veto.
        fireVetoableChange(evt);

        try {
            // Init the byte encoded object.
            final byte[] beo = getEncoded().getBytes(XML_CHARSET);

            // Verify the byte encoded object.
            verificationEngine.initVerify(verificationKey);
            verificationEngine.update(beo);
            final byte[] b64ds = Base64.decodeBase64(getSignature().getBytes(BASE64_CHARSET));
            if (!verificationEngine.verify(b64ds))
                throw new GenericCertificateIntegrityException();

            // Reset signature parameters.
            setSignatureAlgorithm(verificationEngine.getAlgorithm());
            setSignatureEncoding(SIGNATURE_ENCODING);
        } catch (UnsupportedEncodingException cannotHappen) {
            throw new AssertionError(cannotHappen);
        }

        // Lock this certificate and notify property change listeners.
        this.locked = true;
        firePropertyChange(evt);
    }

    /**
     * Returns the "locked" property of this generic certificate.
     *         If <tt>true</tt>, an object was successfully signed or verified
     *         before and a clone can be safely retrieved using
     *         <tt>getContent()</tt>.
     */
    public final boolean isLocked() {
        return this.locked;
    }

    /**
     * Returns a clone of the certificate's content as it was signed or
     * verified before.
     * You should save the returned object for later use as each call
     * to this method is pretty expensive in terms of runtime and
     * memory. This method may return <tt>null</tt> if this has been
     * signed before.
     *
     * @throws GenericCertificateNotLockedException If no content has been
     *         signed or verified before.
     *         Note that this is ultimately a {@link RuntimeException}.
     * @throws PersistenceServiceException If the signed object cannot get
     *         reinstantiated from its XML representation for some reason.
     *         This may happen for example if the signed object was created
     *         by a more recent version of its class which contains additional
     *         properties which are not supported by earlier versions.
     */
    public Object getContent() throws GenericCertificateNotLockedException, PersistenceServiceException {
        if (!isLocked())
            throw new GenericCertificateNotLockedException();

        return PersistenceService.load(getEncoded());
    }

    /**
     * Getter for the property <tt>encoded</tt>.
     * The default is <tt>null</tt>.
     *
     * @return Value of property encoded.
     */
    public final String getEncoded() {
        return this.encoded;
    }

    /**
     * Setter for the bound property <tt>encoded</tt>.
     *
     * @param encoded The new encoded representation of the signed object
     *        - may be <tt>null</tt>.
     *
     * @throws GenericCertificateIsLockedException If this certificate is
     *         already locked by signing or verifying it before.
     *         Note that this is actually a subclass of
     *         {@link PropertyVetoException}.
     */
    public synchronized void setEncoded(String encoded) throws GenericCertificateIsLockedException {
        // Check parameters.
        if (encoded == this.encoded)
            return;

        // Check lock status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "encoded", getEncoded(), encoded); // NOI18N
        if (isLocked())
            throw new GenericCertificateIsLockedException(evt);

        //vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
        this.encoded = encoded;
        firePropertyChange(evt);
    }

    /**
     * Getter for the property <tt>signature</tt>.
     * The default is <tt>null</tt>.
     *
     * @return Value of property signature.
     */
    public final String getSignature() {
        return this.signature;
    }

    /**
     * Setter for the bound property <tt>signature</tt>.
     *
     * @param signature The signature encoded as a string
     *        - may be <tt>null</tt>.
     *
     * @throws GenericCertificateIsLockedException If this certificate is
     *         already locked by signing or verifying it before.
     *         Note that this is actually a subclass of
     *         {@link PropertyVetoException}.
     */
    public synchronized void setSignature(String signature) throws GenericCertificateIsLockedException {
        // Check parameters.
        if (signature == this.signature)
            return;

        // Check lock status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "signature", getSignature(), signature); // NOI18N
        if (isLocked())
            throw new GenericCertificateIsLockedException(evt);

        //vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
        this.signature = signature;
        firePropertyChange(evt);
    }

    /**
     * Getter for the property <tt>signatureAlgorithm</tt>.
     * The default is <tt>null</tt>.
     *
     * @return The signature algorithm.
     */
    public final String getSignatureAlgorithm() {
        return this.signatureAlgorithm;
    }

    /**
     * Setter for the bound property <tt>signatureAlgorithm</tt>.
     *
     * @param signatureAlgorithm The string identifying the signature algorithm
     *        - may be <tt>null</tt>.
     *
     * @throws GenericCertificateIsLockedException If this certificate is
     *         already locked by signing or verifying it before.
     *         Note that this is actually a subclass of
     *         {@link PropertyVetoException}.
     */
    public synchronized void setSignatureAlgorithm(String signatureAlgorithm)
            throws GenericCertificateIsLockedException {
        // Check parameters.
        if (signatureAlgorithm == this.signatureAlgorithm)
            return;

        // Check lock status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "signatureAlgorithm", getSignatureAlgorithm(),
                signatureAlgorithm); // NOI18N
        if (isLocked())
            throw new GenericCertificateIsLockedException(evt);

        //vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
        this.signatureAlgorithm = signatureAlgorithm;
        firePropertyChange(evt);
    }

    /**
     * Getter for the property <tt>signatureEncoding</tt>.
     * The default is <tt>null</tt>.
     *
     * @return The character encoding of the signature string.
     */
    public final String getSignatureEncoding() {
        return signatureEncoding;
    }

    /**
     * Setter for the bound property <tt>signatureEncoding</tt>.
     *
     * @param signatureEncoding The string identifying the signature encoding
     *        - may be <tt>null</tt>.
     *
     * @throws GenericCertificateIsLockedException If this certificate is
     *         already locked by signing or verifying it before.
     *         Note that this is actually a subclass of
     *         {@link PropertyVetoException}.
     *
     * @deprecated Currently ignored by {@link #verify}.
     *             Only provided to cause {@link XMLEncoder} to encode this
     *             property for upwards compatibility.
     */
    public synchronized void setSignatureEncoding(String signatureEncoding)
            throws GenericCertificateIsLockedException {
        // Check parameters.
        if (signatureEncoding == this.signatureEncoding)
            return;

        // Check lock status.
        final PropertyChangeEvent evt = new PropertyChangeEvent(this, "signatureEncoding", getSignatureEncoding(),
                signatureEncoding); // NOI18N
        if (isLocked())
            throw new GenericCertificateIsLockedException(evt);

        //vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
        this.signatureEncoding = signatureEncoding;
        firePropertyChange(evt);
    }

    //
    // Property handling methods.
    //

    /**
     * Adds a VetoableChangeListener to the listener list.
     *
     * @param l The listener to add.
     */
    public final synchronized void addVetoableChangeListener(java.beans.VetoableChangeListener l) {
        if (vetoableChangeSupport == null)
            vetoableChangeSupport = new VetoableChangeSupport(this);
        vetoableChangeSupport.addVetoableChangeListener(l);
    }

    /**
     * Removes a VetoableChangeListener from the listener list.
     *
     * @param l The listener to remove.
     */
    public final void removeVetoableChangeListener(java.beans.VetoableChangeListener l) {
        if (vetoableChangeSupport == null)
            return;
        vetoableChangeSupport.removeVetoableChangeListener(l);
    }

    protected final void fireVetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
        if (vetoableChangeSupport == null)
            return;
        vetoableChangeSupport.fireVetoableChange(evt);
    }

    /**
     * Adds a PropertyChangeListener to the listener list.
     *
     * @param l The listener to add.
     */
    public final synchronized void addPropertyChangeListener(PropertyChangeListener l) {
        if (propertyChangeSupport == null)
            propertyChangeSupport = new PropertyChangeSupport(this);
        propertyChangeSupport.addPropertyChangeListener(l);
    }

    /**
     * Removes a PropertyChangeListener from the listener list.
     *
     * @param l The listener to remove.
     */
    public final void removePropertyChangeListener(PropertyChangeListener l) {
        if (propertyChangeSupport == null)
            return;
        propertyChangeSupport.removePropertyChangeListener(l);
    }

    protected final void firePropertyChange(PropertyChangeEvent evt) {
        if (propertyChangeSupport == null)
            return;
        propertyChangeSupport.firePropertyChange(evt);
    }
}