
de.schlichtherle.xml.GenericCertificate Maven / Gradle / Ivy
Show all versions of truelicense-xml Show documentation
/*
* Copyright (C) 2005-2015 Schlichtherle IT Services.
* All rights reserved. Use is subject to license terms.
*/
package de.schlichtherle.xml;
import java.beans.*;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.*;
import org.apache.commons.codec.binary.Base64;
/**
* This non-visual JavaBean is a factory for authenticated 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}.
*
* More specifically, a {@code GenericCertificate} 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}.
*
* For an object to be successfully digitally signed, it must support
* serialization via JDK's XMLEncoder, for which this package
* provides the class {@code 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.
*
* 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.
*
* The underlying signing algorithm is designated by the Signature
* object passed to the {@code sign} and the {@code verify} methods.
*
* A typical usage for signing is the following:
* {@code
* 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) {
* // ...
* }
*
}
* A typical usage for verification is the following (having
* received GenericCertificate {@code cert}):
* {@code
* 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();
*
}
* Several points are worth noting:
* -
* 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.
* -
* 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()}.
* -
* 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 {@code true} on
* verification in an attempt to bypass a security check.
* -
* The signature algorithm could 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.
* -
* 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.
* - The property change listeners are not persistet when
* using {@link ObjectOutputStream} or {@link XMLEncoder}.
* - {@link Object#equals(Object)} and {@link Object#hashCode()} are
* not overridden by this class because different JVMs will produce
* different literal encodings of the same object and we cannot rely on a proper
* {@code equals(...)} implementation in the class of a signed object.
*
* Potential applications of {@code GenericCertificate} include:
*
* - 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.
* - 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).
* - Nested {@code GenericCertificates} can be used to construct a logical
* sequence of signatures, resembling a chain of authorization and
* delegation.
*
*
* This class is 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 long serialVersionUID = 6247620498526484734L;
private static final String BASE64_CHARSET = "US-ASCII"; // NOI18N
private static final String SIGNATURE_ENCODING = "US-ASCII/Base64"; // NOI18N
/**
* Holds value of property locked - is not serializable!!!
*/
private transient volatile 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;
/** Constructs 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.
*
* @param cert the generic certificate to copy.
*/
public GenericCertificate(final GenericCertificate cert) {
try {
setEncoded(cert.getEncoded());
setSignature(cert.getSignature());
setSignatureAlgorithm(cert.getSignatureAlgorithm());
setSignatureEncoding(cert.getSignatureEncoding());
} catch (PropertyVetoException ex) {
throw new AssertionError(ex);
}
}
/**
* Encodes and signs the given {@code content} in this certificate and
* locks it.
*
* Please note the following:
*
* - This method will throw a {@code PropertyVetoException} if this
* certificate is already locked, i.e. if it has been signed or
* verified before.
* - 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 {@code PropertyVetoException}.
* 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.
* - There is no way to unlock this certificate.
* Call the copy constructor of {@link GenericCertificate} if you
* need an unlocked copy of the certificate.
*
*
* @param content The object to sign. This must either be a JavaBean or an
* instance of any other class which is supported by
* {@code {@link PersistenceService}}
* - maybe {@code null}.
* @param signingKey The private key for signing
* - may not be {@code null}.
* @param signingEngine The signature signing engine
* - may not be {@code null}.
*
* @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 void sign(
final Object content,
final PrivateKey signingKey,
final Signature signingEngine)
throws NullPointerException,
GenericCertificateIsLockedException,
PropertyVetoException,
PersistenceServiceException,
InvalidKeyException {
// Check status.
final PropertyChangeEvent evt = new PropertyChangeEvent(
this, "locked", Boolean.valueOf(locked), Boolean.TRUE); // NOI18N
if (locked) throw new GenericCertificateIsLockedException(evt);
// Check parameters.
if (null == signingKey || null == signingEngine)
throw new NullPointerException();
// Notify vetoable listeners and give them a chance to veto.
fireVetoableChange(evt);
try {
// Encode the object to bytes and sign it.
final byte[] beo = PersistenceService.store2ByteArray(content);
final String encoded = new String(beo, XML_CHARSET);
signingEngine.initSign(signingKey);
signingEngine.update(beo);
final byte[] b64es = Base64.encodeBase64(signingEngine.sign());
final String signature = new String(b64es, BASE64_CHARSET);
final String algorithm = signingEngine.getAlgorithm();
// Store results.
setEncoded(encoded);
setSignature(signature);
setSignatureAlgorithm(algorithm);
setSignatureEncoding(SIGNATURE_ENCODING); // NOI18N
} catch (UnsupportedEncodingException ex) {
throw new AssertionError(ex);
} catch (SignatureException ex) {
throw new AssertionError(ex);
}
// 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.
*
* Please note the following:
*
* - This method will throw a {@code PropertyVetoException} if this
* certificate is already locked, i.e. if it has been signed or
* verified before.
* - 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 {@code PropertyVetoException}.
* 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.
* - There is no way to unlock this certificate.
* Call the copy constructor of {@link GenericCertificate} if you
* need an unlocked copy of the certificate.
*
*
* @param verificationKey The public key for verification
* - may not be {@code null}.
* @param verificationEngine The signature verification engine
* - may not be {@code null}.
*
* @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 void verify(
final PublicKey verificationKey,
final Signature verificationEngine)
throws NullPointerException,
GenericCertificateIsLockedException,
PropertyVetoException,
InvalidKeyException,
SignatureException,
GenericCertificateIntegrityException {
// Check status.
final PropertyChangeEvent evt = new PropertyChangeEvent(
this, "locked", Boolean.valueOf(locked), Boolean.TRUE); // NOI18N
if (locked) throw new GenericCertificateIsLockedException(evt);
// Check parameters.
if (null == verificationKey || null == verificationEngine)
throw new NullPointerException();
// Notify vetoable listeners and give them a chance to veto.
fireVetoableChange(evt);
try {
// Get the byte encoded object and verify it.
final byte[] beo = encoded.getBytes(XML_CHARSET);
verificationEngine.initVerify(verificationKey);
verificationEngine.update(beo);
final byte[] b64ds = Base64.decodeBase64(
signature.getBytes(BASE64_CHARSET));
if (!verificationEngine.verify(b64ds))
throw new GenericCertificateIntegrityException();
final String algorithm = verificationEngine.getAlgorithm();
// Reset signature parameters.
setSignatureAlgorithm(algorithm);
setSignatureEncoding(SIGNATURE_ENCODING);
} catch (UnsupportedEncodingException ex) {
throw new AssertionError(ex);
}
// Lock this certificate and notify property change listeners.
this.locked = true;
firePropertyChange(evt);
}
/**
* Returns the value of the property {@code locked}.
* If {@code true}, an object was successfully signed or verified
* before and a clone can be safely retrieved using
* {@code getContent()}.
*
* @return The value of the property {@code locked}.
*/
public 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 {@code null} if this has been
* signed before.
*
* @return A clone of the certificate's content as it was signed or
* verified 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 synchronized Object getContent()
throws GenericCertificateNotLockedException,
PersistenceServiceException {
if (!locked) throw new GenericCertificateNotLockedException();
return PersistenceService.load(encoded);
}
/**
* The value of the property {@code encoded}.
* The default is {@code null}.
*
* @return The value of the property {@code encoded}.
*/
public synchronized String getEncoded() {
return this.encoded;
}
/**
* Setter for the bound property {@code encoded}.
*
* @param encoded The new encoded representation of the signed object
* - may be {@code null}.
* @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(final String encoded)
throws GenericCertificateIsLockedException {
// Check status.
final PropertyChangeEvent evt = new PropertyChangeEvent(
this, "encoded", this.encoded, encoded); // NOI18N
if (locked) throw new GenericCertificateIsLockedException(evt);
// Check parameters.
if (equals(this.encoded, encoded)) return;
//vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
this.encoded = encoded;
firePropertyChange(evt);
}
/**
* Getter for the property {@code signature}.
* The default is {@code null}.
*
* @return Value of property signature.
*/
public synchronized String getSignature() {
return this.signature;
}
/**
* Setter for the bound property {@code signature}.
*
* @param signature The signature encoded as a string
* - may be {@code null}.
*
* @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(final String signature)
throws GenericCertificateIsLockedException {
// Check status.
final PropertyChangeEvent evt = new PropertyChangeEvent(
this, "signature", this.signature, signature); // NOI18N
if (locked) throw new GenericCertificateIsLockedException(evt);
// Check parameters.
if (equals(this.signature, signature)) return;
//vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
this.signature = signature;
firePropertyChange(evt);
}
/**
* Getter for the property {@code signatureAlgorithm}.
* The default is {@code null}.
*
* @return The signature algorithm.
*/
public synchronized String getSignatureAlgorithm() {
return this.signatureAlgorithm;
}
/**
* Setter for the bound property {@code signatureAlgorithm}.
*
* @param signatureAlgorithm The string identifying the signature algorithm
* - may be {@code null}.
*
* @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(final String signatureAlgorithm)
throws GenericCertificateIsLockedException {
// Check status.
final PropertyChangeEvent evt = new PropertyChangeEvent(
this, "signatureAlgorithm", this.signatureAlgorithm, signatureAlgorithm); // NOI18N
if (locked) throw new GenericCertificateIsLockedException(evt);
// Check parameters.
if (equals(this.signatureAlgorithm, signatureAlgorithm)) return;
//vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
this.signatureAlgorithm = signatureAlgorithm;
firePropertyChange(evt);
}
/**
* Getter for the property {@code signatureEncoding}.
* The default is {@code null}.
*
* @return The character encoding of the signature string.
*/
public synchronized String getSignatureEncoding() {
return signatureEncoding;
}
/**
* Setter for the bound property {@code signatureEncoding}.
*
* @param signatureEncoding The string identifying the signature
* encoding - may be {@code null}.
* @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(final String signatureEncoding)
throws GenericCertificateIsLockedException {
// Check status.
final PropertyChangeEvent evt = new PropertyChangeEvent(
this, "signatureEncoding", this.signatureEncoding, signatureEncoding); // NOI18N
if (locked) throw new GenericCertificateIsLockedException(evt);
// Check parameters.
if (equals(this.signatureEncoding, signatureEncoding)) return;
//vetoableChangeSupport.fireVetoableChange(evt); // Incompatible to sign!
this.signatureEncoding = signatureEncoding;
firePropertyChange(evt);
}
private static boolean equals(Object a, Object b) {
return a == b || null != a && a.equals(b);
}
//
// Property handling methods.
//
/**
* Adds a VetoableChangeListener to the listener list.
*
* @param l The listener to add.
* @deprecated Not required.
*/
public synchronized void addVetoableChangeListener(java.beans.VetoableChangeListener l) {
if (null == vetoableChangeSupport)
vetoableChangeSupport = new VetoableChangeSupport(this);
vetoableChangeSupport.addVetoableChangeListener(l);
}
/**
* Removes a VetoableChangeListener from the listener list.
*
* @param l The listener to remove.
* @deprecated Not required.
*/
public synchronized void removeVetoableChangeListener(java.beans.VetoableChangeListener l) {
if (null == vetoableChangeSupport) return;
vetoableChangeSupport.removeVetoableChangeListener(l);
}
/**
* @deprecated Not required.
*/
private void fireVetoableChange(PropertyChangeEvent evt)
throws PropertyVetoException {
if (null == vetoableChangeSupport) return;
vetoableChangeSupport.fireVetoableChange(evt);
}
/**
* Adds a PropertyChangeListener to the listener list.
*
* @param l The listener to add.
* @deprecated Not required.
*/
public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
if (null == propertyChangeSupport)
propertyChangeSupport = new PropertyChangeSupport(this);
propertyChangeSupport.addPropertyChangeListener(l);
}
/**
* Removes a PropertyChangeListener from the listener list.
*
* @param l The listener to remove.
* @deprecated Not required.
*/
public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
if (null == propertyChangeSupport) return;
propertyChangeSupport.removePropertyChangeListener(l);
}
/**
* @deprecated Not required.
*/
private void firePropertyChange(PropertyChangeEvent evt) {
if (null == propertyChangeSupport) return;
propertyChangeSupport.firePropertyChange(evt);
}
}