/*
* Copyright (c) 2001-2007 Sun Microsystems, Inc. All rights reserved.
*
* The Sun Project JXTA(TM) Software License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The end-user documentation included with the redistribution, if any, must
* include the following acknowledgment: "This product includes software
* developed by Sun Microsystems, Inc. for JXTA(TM) technology."
* Alternately, this acknowledgment may appear in the software itself, if
* and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must
* not be used to endorse or promote products derived from this software
* without prior written permission. For written permission, please contact
* Project JXTA at http://www.jxta.org.
*
* 5. Products derived from this software may not be called "JXTA", nor may
* "JXTA" appear in their name, without prior written permission of Sun.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN
* MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* JXTA is a registered trademark of Sun Microsystems, Inc. in the United
* States and other countries.
*
* Please see the license information page at :
* <http://www.jxta.org/project/www/license.html> for instructions on use of
* the license in source files.
*
* ====================================================================
*
* This software consists of voluntary contributions made by many individuals
* on behalf of Project JXTA. For more information on Project JXTA, please see
* http://www.jxta.org.
*
* This license is based on the BSD license adopted by the Apache Foundation.
*/
package net.jxta.impl.membership.pse;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jxta.credential.Credential;
import net.jxta.credential.CredentialPCLSupport;
import net.jxta.document.Attributable;
import net.jxta.document.Attribute;
import net.jxta.document.Element;
import net.jxta.document.MimeMediaType;
import net.jxta.document.StructuredDocument;
import net.jxta.document.StructuredDocumentFactory;
import net.jxta.document.StructuredDocumentUtils;
import net.jxta.document.XMLDocument;
import net.jxta.document.XMLElement;
import net.jxta.exception.PeerGroupException;
import net.jxta.id.ID;
import net.jxta.id.IDFactory;
import net.jxta.impl.util.TimeUtils;
import net.jxta.impl.util.threads.TaskManager;
import net.jxta.logging.Logging;
import net.jxta.peer.PeerID;
import net.jxta.peergroup.PeerGroupID;
import net.jxta.service.Service;
/**
* This class provides the sub-class of Credential which is associated with the
* PSE membership service.
* <p/>
* There are two varients of the credential:
* <p/>
* <ul>
* <li>local - Generated as a result of local login. This type of
* credential can be used for signing and can be serialized for inclusion
* in protocols.</li>
* <li>remote - Generated as a result of deserialization from protocols.
* The credential is verified to ensure that the contents are valid at the
* time it is created.</li>
* </ul>
* <p/>
* The schema for this credential format:
* <p/>
* <pre><code>
* <xs:element name="PSECred" type="jxta:PSECred" />
* <p/>
* <xs:complexType name="PSECred">
* <xs:sequence>
* <xs:element name="PeerGroupID" type="jxta:JXTAID" />
* <xs:element name="PeerID" type="jxta:JXTAID" />
* <!-- An X.509 Certificate -->
* <xs:element name="Certificate" type="xs:string" minOccurs="1" maxOccurs="unbounded" />
* <!-- A SHA1WithRSA Signature -->
* <xs:element name="Signature" type="xs:string" />
* </xs:sequence>
* </xs:complexType>
* </code></pre>
* <p/>
* FIXME 20050625 bondolo If the certificate chain for a credential is
* updated in the PSE keystore after a credential is created then the
* credential instance will not reflect those changes. This can be a problem if
* the issuer chain changes or expiries are updated. Even though it's going to
* be hit on performance PSECredential needs to changed to be backed by the PSE
* keystore directly rather than containing the certs. Either that or some kind
* of notification systems. It's probably best to assume that our simple cm
* based keystore is the easiest and least dynamic case. Every other key store
* is going to be more dynamic and difficult. The work around for now is to
* force a membership resign everytime the keystore contents are changed.
*
* @see net.jxta.credential.Credential
* @see net.jxta.impl.membership.pse.PSEMembershipService
*/
public final class PSECredential implements Credential, CredentialPCLSupport {
/**
* Logger
*/
private static final Logger LOG = Logger.getLogger(PSECredential.class.getName());
/**
* The MembershipService service which generated this credential.
* <p/>
* XXX 20030609 bondolo@jxta.org Perhaps this should be a weak reference.
*/
private PSEMembershipService source;
/**
* The peer group associated with this credential.
*/
private ID peerGroupID = null;
/**
* The peerid associated with this credential.
*/
private ID peerID = null;
/**
* The pse alias from which this credential was generated. Only locally
* created credentials will be intialized with a key ID.
*/
private ID keyID = null;
/**
* The identity associated with this credential
*/
private CertPath certs = null;
/**
* The private key associated with this credential. Used for signing. Only
* a locally created credential will have an initialized private key.
*/
private PrivateKey privateKey = null;
/**
* Optional Timer task
*/
private ScheduledFuture<?> becomesValidTaskHandle = null;
private ScheduledFuture<?> expiresTaskHandle = null;
/**
* Are we still a valid credential?
*/
private boolean valid = true;
/**
* Is this a local credential?
*/
private final boolean local;
/**
* property change support
*/
private PropertyChangeSupport support = new PropertyChangeSupport(this);
/**
* Create a new local credential. This credential can be used for signing
* and can be serialized.
*/
protected PSECredential(PSEMembershipService source, ID keyID, CertPath certChain, PrivateKey privateKey) throws IOException {
this.source = source;
this.peerID = source.group.getPeerID();
this.peerGroupID = source.group.getPeerGroupID();
setKeyID(keyID);
setCertificateChain(certChain);
setPrivateKey(privateKey);
this.local = true;
}
/**
* Create a new remote credential. This credential cannot be used for
* signing and cannot be re-serialized.
*/
public PSECredential(Element root) {
this.local = false;
initialize(root);
}
/**
* Create a new remote credential. This credential cannot be used for
* signing and cannot be re-serialized.
*/
public PSECredential(PSEMembershipService source, Element root) {
this.local = false;
this.source = source;
initialize(root);
if (!peerGroupID.equals(source.group.getPeerGroupID())) {
throw new IllegalArgumentException(
"Credential is from a different group. " + peerGroupID + " != " + source.group.getPeerGroupID());
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object target) {
if (this == target) {
return true;
}
if (target instanceof PSECredential) {
PSECredential asCred = (PSECredential) target;
boolean result = peerID.equals(asCred.peerID)
&& source.group.getPeerGroupID().equals(asCred.source.group.getPeerGroupID());
result &= certs.equals(asCred.certs);
return result;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
protected void finalize() throws Throwable {
if (null != becomesValidTaskHandle) {
becomesValidTaskHandle.cancel(false);
}
if (null != expiresTaskHandle) {
expiresTaskHandle.cancel(false);
}
super.finalize();
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int result = peerID.hashCode() * source.group.getPeerGroupID().hashCode() * certs.hashCode();
if (0 == result) {
result = 1;
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "\"" + getSubject() + "\" " + getPeerID() + " [" + source + " / " + getPeerGroupID() + "]";
}
/**
* Add a listener
*
* @param listener the listener
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
/**
* Add a listener
*
* @param propertyName the property to watch
* @param listener the listener
*/
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
support.addPropertyChangeListener(propertyName, listener);
}
/**
* Remove a listener
*
* @param listener the listener
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
/**
* Remove a listener
*
* @param propertyName the property which was watched
* @param listener the listener
*/
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
support.removePropertyChangeListener(propertyName, listener);
}
/**
* {@inheritDoc}
*/
public ID getPeerGroupID() {
return peerGroupID;
}
/**
* set the peer id associated with this credential
*/
private void setPeerGroupID(ID newID) {
this.peerGroupID = newID;
}
/**
* {@inheritDoc}
*/
public ID getPeerID() {
return peerID;
}
/**
* set the peer id associated with this credential
*/
private void setPeerID(PeerID peerID) {
this.peerID = peerID;
}
/**
* {@inheritDoc}
* <p/>
* A PSE Credential is valid as long as the associated certificate is
* valid.
*/
public boolean isExpired() {
try {
((X509Certificate) certs.getCertificates().get(0)).checkValidity();
return false;
} catch (CertificateExpiredException expired) {
return true;
} catch (CertificateNotYetValidException notyet) {
return true;
}
}
/**
* {@inheritDoc}
* <p/>
* A PSE Credential is valid as long as the associated certificate is
* valid and as long as the membership service still has the credential.
*/
public boolean isValid() {
return valid && !isExpired();
}
/**
* Sets whether or not the credential is valid.
* <p/>
* A PSE Credential is valid as long as the associated certificate is
* valid.
*
* @param valid {@code true} if it is valid, {@code false} otherwise
*/
void setValid(boolean valid) {
boolean oldValid = isValid();
this.valid = valid;
if (oldValid != valid) {
support.firePropertyChange("valid", oldValid, valid);
}
}
/**
* {@inheritDoc}
*/
public Object getSubject() {
return ((X509Certificate) certs.getCertificates().get(0)).getSubjectDN();
}
/**
* {@inheritDoc}
*/
public Service getSourceService() {
return source;
}
/**
* {@inheritDoc}
*/
public StructuredDocument getDocument(MimeMediaType encodeAs) throws Exception {
if (!isValid()) {
throw new javax.security.cert.CertificateException("Credential is not valid. Cannot generate document.");
}
if (!local) {
throw new IllegalStateException("This credential is not a local credential and document cannot be created.");
}
StructuredDocument doc = StructuredDocumentFactory.newStructuredDocument(encodeAs, "jxta:Cred");
if (doc instanceof XMLDocument) {
((XMLDocument) doc).addAttribute("xmlns:jxta", "http://jxta.org");
((XMLDocument) doc).addAttribute("xml:space", "preserve");
}
if (doc instanceof Attributable) {
((Attributable) doc).addAttribute("type", "jxta:PSECred");
}
Element e;
e = doc.createElement("PeerGroupID", getPeerGroupID().toString());
doc.appendChild(e);
e = doc.createElement("PeerID", getPeerID().toString());
doc.appendChild(e);
// add the Certificate element
net.jxta.impl.protocol.Certificate certChain = new net.jxta.impl.protocol.Certificate();
List certsList = certs.getCertificates();
certChain.setCertificates(certsList);
StructuredDocument certsDoc = (StructuredDocument) certChain.getDocument(encodeAs);
if (certsDoc instanceof Attributable) {
((Attributable) certsDoc).addAttribute("type", certsDoc.getKey().toString());
}
StructuredDocumentUtils.copyElements(doc, doc, certsDoc, "Certificate");
// Add the signature.
List someStreams = new ArrayList(3);
try {
someStreams.add(new ByteArrayInputStream(getPeerGroupID().toString().getBytes("UTF-8")));
someStreams.add(new ByteArrayInputStream(getPeerID().toString().getBytes("UTF-8")));
for (Object aCertsList : certsList) {
X509Certificate aCert = (X509Certificate) aCertsList;
someStreams.add(new ByteArrayInputStream(aCert.getEncoded()));
}
InputStream signStream = new SequenceInputStream(Collections.enumeration(someStreams));
byte[] sig = source.peerSecurityEngine.sign(source.peerSecurityEngine.getSignatureAlgorithm(), this, signStream);
e = doc.createElement("Signature", PSEUtils.base64Encode(sig));
doc.appendChild(e);
} catch (java.io.UnsupportedEncodingException never) {// UTF-8 is always available
}
if (doc instanceof Attributable) {
((Attributable) doc).addAttribute("algorithm", source.peerSecurityEngine.getSignatureAlgorithm());
}
return doc;
}
/**
* Returns the certificate associated with this credential.
*
* @return the certificate associated with this credential.
*/
public X509Certificate getCertificate() {
return (X509Certificate) certs.getCertificates().get(0);
}
/**
* Returns the certificate chain associated with this credential.
*
* @return the certificate chain associated with this credential.
*/
public X509Certificate[] getCertificateChain() {
List certList = certs.getCertificates();
return (X509Certificate[]) certList.toArray(new X509Certificate[certList.size()]);
}
/**
* Set the certificate associated with this credential
*
* @param certChain the certificate chain associated with this credential.
*/
private void setCertificateChain(CertPath certChain) {
certs = certChain;
Date now = new Date();
Date becomesValid = ((X509Certificate) certs.getCertificates().get(0)).getNotBefore();
Date expires = ((X509Certificate) certs.getCertificates().get(0)).getNotAfter();
if (becomesValid.compareTo(now) > 0) {
if (null != becomesValidTaskHandle) {
becomesValidTaskHandle.cancel(false);
}
Runnable becomesValidTask = new Runnable() {
public void run() {
support.firePropertyChange("expired", false, true);
if (valid) {
support.firePropertyChange("valid", false, true);
}
}
};
ScheduledExecutorService executor = TaskManager.getTaskManager().getScheduledExecutorService();
long delay = TimeUtils.toRelativeTimeMillis(becomesValid.getTime());
becomesValidTaskHandle = executor.schedule(becomesValidTask, delay, TimeUnit.MILLISECONDS);
}
if (null != expiresTaskHandle) {
expiresTaskHandle.cancel(false);
}
if (expires.compareTo(now) > 0) {
Runnable expiresTask = new Runnable() {
public void run() {
support.firePropertyChange("expired", true, false);
if (valid) {
support.firePropertyChange("valid", true, false);
}
}
};
ScheduledExecutorService executor = TaskManager.getTaskManager().getScheduledExecutorService();
long delay = TimeUtils.toRelativeTimeMillis(expires.getTime());
expiresTaskHandle = executor.schedule(expiresTask, delay, TimeUnit.MILLISECONDS);
}
boolean nowGood = (null == becomesValidTaskHandle) && (null != expiresTaskHandle);
support.firePropertyChange("expired", true, nowGood);
setValid(nowGood);
}
/**
* Returns the private key associated with this credential. Only valid for
* locally generated credentials.
*
* @return the private key associated with this credential.
* @deprecated Use <@link #getSigner(String)> or <@link #getSignatureVerifier(String)> instead.
*/
@Deprecated
public PrivateKey getPrivateKey() {
if (!local) {
throw new IllegalStateException("This credential is not a local credential and cannot be used for signing.");
}
if (null == privateKey) {
throw new IllegalStateException("This local credential is engine based and cannot provide the private key.");
}
return privateKey;
}
/**
* Sets the private key associated with this credential.
*
* @param privateKey the private key associated with this credential.
*/
private void setPrivateKey(PrivateKey privateKey) {
this.privateKey = privateKey;
}
/**
* Returns the key id associated with this credential, if any. Only locally
* generated credentials have a key ID.
*
* @return Returns the key id associated with this credential, if any.
*/
public ID getKeyID() {
return keyID;
}
/**
* Sets the key id associated with this credential.
*/
private void setKeyID(ID keyID) {
this.keyID = keyID;
}
/**
* Get a Signature object based upon the private key associated with this
* credential.
*
* @param algorithm the signing algorithm to use.
* @return Signature.
*/
public Signature getSigner(String algorithm) throws NoSuchAlgorithmException {
if (!local) {
throw new IllegalStateException("This credential is not a local credential and cannot be used for signing.");
}
Signature sign = Signature.getInstance(algorithm);
try {
sign.initSign(privateKey);
} catch (java.security.InvalidKeyException failed) {
IllegalStateException failure = new IllegalStateException("Invalid private key");
failure.initCause(failed);
throw failure;
}
return sign;
}
/**
* /**
* Get a Signature verifier object based upon the certificate associated
* with this credential.
*
* @param algorithm the signing algorithm to use.
* @return Signature.
*/
public Signature getSignatureVerifier(String algorithm) throws NoSuchAlgorithmException {
Signature verify = Signature.getInstance(algorithm);
try {
verify.initVerify((X509Certificate) certs.getCertificates().get(0));
} catch (java.security.InvalidKeyException failed) {
IllegalStateException failure = new IllegalStateException("Invalid certificate");
failure.initCause(failed);
throw failure;
}
return verify;
}
/**
* Process an individual element from the document.
*
* @param elem the element to be processed.
* @return true if the element was recognized, otherwise false.
*/
protected boolean handleElement(XMLElement elem) {
if (elem.getName().equals("PeerGroupID")) {
try {
ID pid = IDFactory.fromURI(new URI(elem.getTextValue()));
setPeerGroupID((PeerGroupID) pid);
} catch (URISyntaxException badID) {
throw new IllegalArgumentException("Bad PeerGroupID in advertisement: " + elem.getTextValue());
} catch (ClassCastException badID) {
throw new IllegalArgumentException("Id is not a group id: " + elem.getTextValue());
}
return true;
}
if (elem.getName().equals("PeerID")) {
try {
ID pid = IDFactory.fromURI(new URI(elem.getTextValue()));
setPeerID((PeerID) pid);
} catch (URISyntaxException badID) {
throw new IllegalArgumentException("Bad Peer ID in advertisement: " + elem.getTextValue());
} catch (ClassCastException badID) {
throw new IllegalArgumentException("Id is not a peer id: " + elem.getTextValue());
}
return true;
}
if (elem.getName().equals("Certificate")) {
// XXX Compatibility hack so that net.jxta.impl.protocol.Certificate will recognize element
// as a certificate.
if (null == elem.getAttribute("type")) {
elem.addAttribute("type", net.jxta.impl.protocol.Certificate.getMessageType());
}
net.jxta.impl.protocol.Certificate certChain = new net.jxta.impl.protocol.Certificate(elem);
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
certs = cf.generateCertPath(Arrays.asList(certChain.getCertificates()));
} catch (java.security.cert.CertificateException failure) {
throw new IllegalArgumentException("bad certificates in chain.");
}
return true;
}
if (elem.getName().equals("Signature")) {
if (null == certs) {
throw new IllegalArgumentException("Signature out of order in Credential.");
}
List<InputStream> someStreams = new ArrayList<InputStream>(3);
try {
byte[] signatureToCompare = PSEUtils.base64Decode(new StringReader(elem.getTextValue()));
someStreams.add(new ByteArrayInputStream(getPeerGroupID().toString().getBytes("UTF-8")));
someStreams.add(new ByteArrayInputStream(getPeerID().toString().getBytes("UTF-8")));
Iterator eachCert = certs.getCertificates().iterator();
for (Certificate certificate : certs.getCertificates()) {
X509Certificate aCert = (X509Certificate) certificate;
someStreams.add(new ByteArrayInputStream(aCert.getEncoded()));
}
InputStream signStream = new SequenceInputStream(Collections.enumeration(someStreams));
// FIXME 20051007 bondolo Fix handling of signature type.
if (!PSEUtils.verifySignature("SHA1WITHRSA", getCertificate(), signatureToCompare, signStream)) {
throw new IllegalArgumentException("Certificated did not match");
}
} catch (Throwable failed) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Failed to validate signature ", failed);
}
throw new IllegalArgumentException("Failed to validate signature " + failed.getMessage());
}
return true;
}
// element was not handled
return false;
}
/**
* Intialize from a portion of a structured document.
*/
protected void initialize(Element root) {
if (!XMLElement.class.isInstance(root)) {
throw new IllegalArgumentException(getClass().getName() + " only supports XMLElement");
}
XMLElement doc = (XMLElement) root;
String typedoctype = "";
Attribute itsType = doc.getAttribute("type");
if (null != itsType) {
typedoctype = itsType.getValue();
}
String doctype = doc.getName();
if (!doctype.equals("jxta:PSECred") && !typedoctype.equals("jxta:PSECred")) {
throw new IllegalArgumentException(
"Could not construct : " + getClass().getName() + "from doc containing a " + doctype);
}
Enumeration elements = doc.getChildren();
while (elements.hasMoreElements()) {
XMLElement elem = (XMLElement) elements.nextElement();
if (!handleElement(elem)) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Unhandled element \'" + elem.getName() + "\' in " + doc.getName());
}
}
}
// sanity check time!
if (null == getSubject()) {
throw new IllegalArgumentException("subject was never initialized.");
}
if (null == getPeerGroupID()) {
throw new IllegalArgumentException("peer group was never initialized.");
}
if (null == getPeerID()) {
throw new IllegalArgumentException("peer id was never initialized.");
}
if (null == certs) {
throw new IllegalArgumentException("certificates were never initialized.");
}
// FIXME bondolo@jxta.org 20030409 should check for duplicate elements and for peergroup element
}
public X509Certificate[] generateServiceCertificate(ID assignedID) throws IOException, KeyStoreException, InvalidKeyException, SignatureException {
return source.generateServiceCertificate(assignedID, this);
}
public PSECredential getServiceCredential(ID assignedID) throws IOException, PeerGroupException, InvalidKeyException, SignatureException {
return source.getServiceCredential(assignedID, this);
}
}