Java tutorial
/* * Copyright 1999-2010 University of Chicago * * 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.globus.gsi; import org.globus.gsi.util.CertificateIOUtil; import org.globus.gsi.util.CertificateLoadUtil; import org.globus.gsi.util.CertificateUtil; import org.globus.gsi.util.ProxyCertificateUtil; import org.globus.gsi.trustmanager.X509ProxyCertPathValidator; import org.globus.gsi.stores.ResourceSigningPolicyStore; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.Log; import java.security.cert.CertStore; import java.security.KeyStore; import org.globus.common.CoGProperties; import java.io.FileNotFoundException; import java.io.FileInputStream; import java.security.cert.CertificateException; import org.globus.gsi.bc.BouncyCastleUtil; import java.security.interfaces.RSAPrivateKey; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Serializable; import java.security.GeneralSecurityException; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Date; import java.util.Vector; import org.bouncycastle.util.encoders.Base64; import org.globus.gsi.stores.Stores; import org.globus.gsi.bc.BouncyCastleOpenSSLKey; /** * FILL ME * <p/> * This class equivalent was called GlobusCredential in CoG -maybe a better name? * * @author ranantha@mcs.anl.gov */ // COMMENT: Added methods from GlobusCredential // COMMENT: Do we need the getDefaultCred functionality? public class X509Credential implements Serializable { private static final long serialVersionUID = 1L; public static final int BUFFER_SIZE = Integer.MAX_VALUE; private static Log logger = LogFactory.getLog(X509Credential.class.getCanonicalName()); private OpenSSLKey opensslKey; private X509Certificate[] certChain; private static X509Credential defaultCred; private static long credentialLastModified = -1; // indicates if default credential was explicitely set // and if so - if the credential expired it try // to load the proxy from a file. private static boolean credentialSet = false; private static File credentialFile = null; static { new ProviderLoader(); } public X509Credential(PrivateKey initKey, X509Certificate[] initCertChain) { if (initKey == null) { throw new IllegalArgumentException("Key cannot be null"); } if ((initCertChain == null) || (initCertChain.length < 1)) { throw new IllegalArgumentException("At least one public certificate required"); } this.certChain = new X509Certificate[initCertChain.length]; System.arraycopy(initCertChain, 0, this.certChain, 0, initCertChain.length); this.opensslKey = new BouncyCastleOpenSSLKey(initKey); } public X509Credential(InputStream certInputStream, InputStream keyInputStream) throws CredentialException { if (certInputStream.markSupported()) { certInputStream.mark(BUFFER_SIZE); } loadKey(keyInputStream); loadCertificate(certInputStream); validateCredential(); } public X509Credential(String certFile, String keyFile) throws CredentialException, IOException { loadKey(new FileInputStream(new File(keyFile))); loadCertificate(new FileInputStream(new File(certFile))); validateCredential(); } public X509Credential(String proxyFile) throws CredentialException { if (proxyFile == null) { throw new IllegalArgumentException("proxy file is null"); } logger.debug("Loading proxy file: " + proxyFile); try { InputStream in = new FileInputStream(proxyFile); load(in); } catch (FileNotFoundException f) { throw new CredentialException("proxy not found"); } } public X509Credential(InputStream input) throws CredentialException { load(input); } public X509Certificate[] getCertificateChain() { X509Certificate[] returnArray = new X509Certificate[this.certChain.length]; System.arraycopy(this.certChain, 0, returnArray, 0, this.certChain.length); return returnArray; } public PrivateKey getPrivateKey() throws CredentialException { return getPrivateKey(null); } public PrivateKey getPrivateKey(String password) throws CredentialException { if (this.opensslKey.isEncrypted()) { if (password == null) { throw new CredentialException("Key encrypted, password required"); } else { try { this.opensslKey.decrypt(password); } catch (GeneralSecurityException exp) { throw new CredentialException(exp.getMessage(), exp); } } } return this.opensslKey.getPrivateKey(); } public boolean isEncryptedKey() { return this.opensslKey.isEncrypted(); } /** * Reads Base64 encoded data from the stream and returns its decoded value. The reading continues until * the "END" string is found in the data. Otherwise, returns null. */ private static byte[] getDecodedPEMObject(BufferedReader reader) throws IOException { String line; StringBuffer buf = new StringBuffer(); while ((line = reader.readLine()) != null) { if (line.indexOf("--END") != -1) { // found end return Base64.decode(buf.toString().getBytes()); } else { buf.append(line); } } throw new EOFException("Missing PEM end footer"); } public void saveKey(OutputStream out) throws IOException { this.opensslKey.writeTo(out); out.flush(); } // COMMENT Used to be "key cert cert cert ...", which is wrong afaik. must be "cert key cert cert ..." public void saveCertificateChain(OutputStream out) throws IOException, CertificateEncodingException { CertificateIOUtil.writeCertificate(out, this.certChain[0]); for (int i = 1; i < this.certChain.length; i++) { // skip the self-signed certificates if (this.certChain[i].getSubjectDN().equals(certChain[i].getIssuerDN())) { continue; } CertificateIOUtil.writeCertificate(out, this.certChain[i]); } out.flush(); } public void save(OutputStream out) throws IOException, CertificateEncodingException { CertificateIOUtil.writeCertificate(out, this.certChain[0]); saveKey(out); for (int i = 1; i < this.certChain.length; i++) { // This will skip the self-signed certificates? if (this.certChain[i].getSubjectDN().equals(certChain[i].getIssuerDN())) { continue; } CertificateIOUtil.writeCertificate(out, this.certChain[i]); } out.flush(); } public void writeToFile(File file) throws IOException, CertificateEncodingException { writeToFile(file, file); } public void writeToFile(File certFile, File keyFile) throws IOException, CertificateEncodingException { FileOutputStream keyOutputStream = null; FileOutputStream certOutputStream = null; try { keyOutputStream = new FileOutputStream(keyFile); certOutputStream = new FileOutputStream(certFile); saveKey(keyOutputStream); saveCertificateChain(certOutputStream); } finally { try { if (keyOutputStream != null) { keyOutputStream.close(); } } catch (IOException e) { logger.warn("Could not close stream on save of key to file. " + keyFile.getPath()); } try { if (certOutputStream != null) { certOutputStream.close(); } } catch (IOException e) { logger.warn("Could not close stream on save certificate chain to file. " + certFile.getPath()); } } } public Date getNotBefore() { Date notBefore = this.certChain[0].getNotBefore(); for (int i = 1; i < this.certChain.length; i++) { Date date = this.certChain[i].getNotBefore(); if (date.before(notBefore)) { notBefore = date; } } return notBefore; } /** * Returns the number of certificates in the credential without the self-signed certificates. * * @return number of certificates without counting self-signed certificates */ public int getCertNum() { for (int i = this.certChain.length - 1; i >= 0; i--) { if (!this.certChain[i].getSubjectDN().equals(this.certChain[i].getIssuerDN())) { return i + 1; } } return this.certChain.length; } /** * Returns strength of the private/public key in bits. * * @return strength of the key in bits. Returns -1 if unable to determine it. */ public int getStrength() throws CredentialException { return getStrength(null); } /** * Returns strength of the private/public key in bits. * * @return strength of the key in bits. Returns -1 if unable to determine it. */ public int getStrength(String password) throws CredentialException { if (opensslKey == null) { return -1; } if (this.opensslKey.isEncrypted()) { if (password == null) { throw new CredentialException("Key encrypted, password required"); } else { try { this.opensslKey.decrypt(password); } catch (GeneralSecurityException exp) { throw new CredentialException(exp.getMessage(), exp); } } } return ((RSAPrivateKey) opensslKey.getPrivateKey()).getModulus().bitLength(); } /** * Returns the subject DN of the first certificate in the chain. * * @return subject DN. */ public String getSubject() { return this.certChain[0].getSubjectDN().getName(); } /** * Returns the issuer DN of the first certificate in the chain. * * @return issuer DN. */ public String getIssuer() { return this.certChain[0].getIssuerDN().getName(); } /** * Returns the certificate type of the first certificate in the chain. Returns -1 if unable to determine * the certificate type (an error occurred) * * @see BouncyCastleUtil#getCertificateType(X509Certificate) * * @return the type of first certificate in the chain. -1 if unable to determine the certificate type. */ public GSIConstants.CertificateType getProxyType() { try { return BouncyCastleUtil.getCertificateType(this.certChain[0]); } catch (CertificateException e) { logger.error("Error getting certificate type.", e); return GSIConstants.CertificateType.UNDEFINED; } } /** * Returns time left of this credential. The time left of the credential is based on the certificate with * the shortest validity time. * * @return time left in seconds. Returns 0 if the certificate has expired. */ public long getTimeLeft() { Date earliestTime = null; for (int i = 0; i < this.certChain.length; i++) { Date time = this.certChain[i].getNotAfter(); if (earliestTime == null || time.before(earliestTime)) { earliestTime = time; } } long diff = (earliestTime.getTime() - System.currentTimeMillis()) / 1000; return (diff < 0) ? 0 : diff; } /** * Returns the identity of this credential. * @see #getIdentityCertificate() * * @return The identity cert in Globus format (e.g. /C=US/..). Null, * if unable to get the identity (an error occurred) */ public String getIdentity() { try { return BouncyCastleUtil.getIdentity(this.certChain); } catch (CertificateException e) { logger.debug("Error getting certificate identity.", e); return null; } } /** * Returns the identity certificate of this credential. The identity certificate is the first certificate * in the chain that is not an impersonation proxy certificate. * * @return <code>X509Certificate</code> the identity cert. Null, if unable to get the identity certificate * (an error occurred) */ public X509Certificate getIdentityCertificate() { try { return BouncyCastleUtil.getIdentityCertificate(this.certChain); } catch (CertificateException e) { logger.debug("Error getting certificate identity.", e); return null; } } /** * Returns the path length constraint. The shortest length in the chain of * certificates is returned as the credential's path length. * * @return The path length constraint of the credential. -1 is any error * occurs. */ public int getPathConstraint() { int pathLength = Integer.MAX_VALUE; try { for (int i = 0; i < this.certChain.length; i++) { int length = BouncyCastleUtil.getProxyPathConstraint(this.certChain[i]); // if length is one, then no proxy cert extension exists, so // path length is -1 if (length == -1) { length = Integer.MAX_VALUE; } if (length < pathLength) { pathLength = length; } } } catch (Exception e) { logger.warn("Error retrieving path length.", e); pathLength = -1; } return pathLength; } /** * Verifies the validity of the credentials. All certificate path validation is performed using trusted * certificates in default locations. * * @exception CredentialException * if one of the certificates in the chain expired or if path validiation fails. */ public void verify() throws CredentialException { try { String caCertsLocation = "file:" + CoGProperties.getDefault().getCaCertLocations(); KeyStore keyStore = Stores.getTrustStore(caCertsLocation + "/" + Stores.getDefaultCAFilesPattern()); CertStore crlStore = Stores.getCRLStore(caCertsLocation + "/" + Stores.getDefaultCRLFilesPattern()); ResourceSigningPolicyStore sigPolStore = Stores .getSigningPolicyStore(caCertsLocation + "/" + Stores.getDefaultSigningPolicyFilesPattern()); X509ProxyCertPathParameters parameters = new X509ProxyCertPathParameters(keyStore, crlStore, sigPolStore, false); X509ProxyCertPathValidator validator = new X509ProxyCertPathValidator(); validator.engineValidate(CertificateUtil.getCertPath(certChain), parameters); } catch (Exception e) { throw new CredentialException(e); } } /** * Returns the default credential. The default credential is usually the user proxy certificate. <BR> * The credential will be loaded on the initial call. It must not be expired. All subsequent calls to this * function return cached credential object. Once the credential is cached, and the underlying file * changes, the credential will be reloaded. * * @return the default credential. * @exception CredentialException * if the credential expired or some other error with the credential. */ public synchronized static X509Credential getDefaultCredential() throws CredentialException { if (defaultCred == null) { reloadDefaultCredential(); } else if (!credentialSet) { if (credentialFile.lastModified() == credentialLastModified) { defaultCred.verify(); } else { defaultCred = null; reloadDefaultCredential(); } } return defaultCred; } private static void reloadDefaultCredential() throws CredentialException { String proxyLocation = CoGProperties.getDefault().getProxyFile(); defaultCred = new X509Credential(proxyLocation); credentialFile = new File(proxyLocation); credentialLastModified = credentialFile.lastModified(); defaultCred.verify(); } /** * Sets default credential. * * @param cred * the credential to set a default. */ public synchronized static void setDefaultCredential(X509Credential cred) { defaultCred = cred; credentialSet = (cred != null); } // COMMENT: In case of an exception because of missing password with an // encrypted key: put in -1 as strength public String toString() { String lineSep = System.getProperty("line.separator"); StringBuffer buf = new StringBuffer(); buf.append("subject : ").append(getSubject()).append(lineSep); buf.append("issuer : ").append(getIssuer()).append(lineSep); int strength = -1; try { strength = this.getStrength(); } catch (Exception e) { } buf.append("strength : ").append(strength).append(lineSep); buf.append("timeleft : ").append(getTimeLeft() + " sec").append(lineSep); buf.append("proxy type : ").append(ProxyCertificateUtil.getProxyTypeAsString(getProxyType())); return buf.toString(); } protected void load(InputStream input) throws CredentialException { if (input == null) { throw new IllegalArgumentException("input stream cannot be null"); } X509Certificate cert = null; Vector chain = new Vector(3); String line; BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(input)); while ((line = reader.readLine()) != null) { if (line.indexOf("BEGIN CERTIFICATE") != -1) { byte[] data = getDecodedPEMObject(reader); cert = CertificateLoadUtil.loadCertificate(new ByteArrayInputStream(data)); chain.addElement(cert); } else if (line.indexOf("BEGIN RSA PRIVATE KEY") != -1) { byte[] data = getDecodedPEMObject(reader); this.opensslKey = new BouncyCastleOpenSSLKey("RSA", data); } } } catch (Exception e) { throw new CredentialException(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { } } } int size = chain.size(); if (size == 0) { throw new CredentialException("no certs"); } if (opensslKey == null) { throw new CredentialException("no key"); } // set chain this.certChain = new X509Certificate[size]; chain.copyInto(certChain); } protected void loadCertificate(InputStream input) throws CredentialException { if (input == null) { throw new IllegalArgumentException("Input stream to load X509Credential is null"); } X509Certificate cert; Vector<X509Certificate> chain = new Vector<X509Certificate>(); String line; BufferedReader reader = null; try { if (input.markSupported()) { input.reset(); } reader = new BufferedReader(new InputStreamReader(input)); while ((line = reader.readLine()) != null) { if (line.indexOf("BEGIN CERTIFICATE") != -1) { byte[] data = getDecodedPEMObject(reader); cert = CertificateLoadUtil.loadCertificate(new ByteArrayInputStream(data)); chain.addElement(cert); } } } catch (IOException e) { throw new CredentialException(e); } catch (GeneralSecurityException e) { throw new CredentialException(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { logger.debug("error closing reader", e); // This is ok } } } int size = chain.size(); if (size > 0) { this.certChain = new X509Certificate[size]; chain.copyInto(this.certChain); } } protected void loadKey(InputStream input) throws CredentialException { // JGLOBUS-95: BC seems to have some PEM utility but the actual // load is in private methods and cannot be leveraged. // Investigate availability of standard libraries for these // low level reads. FOr now, copying from CoG try { this.opensslKey = new BouncyCastleOpenSSLKey(input); } catch (IOException e) { throw new CredentialException(e.getMessage(), e); } catch (GeneralSecurityException e) { throw new CredentialException(e.getMessage(), e); } } private void validateCredential() throws CredentialException { if (this.certChain == null) { throw new CredentialException("No certificates found"); } int size = this.certChain.length; if (size < 0) { throw new CredentialException("No certificates found."); } if (this.opensslKey == null) { throw new CredentialException("NO private key found"); } } @Override public boolean equals(Object object) { if (object == this) { return true; } if (!(object instanceof X509Credential)) { return false; } X509Credential other = (X509Credential) object; return Arrays.equals(this.certChain, other.certChain) && this.opensslKey.equals(other.opensslKey); } @Override public int hashCode() { return (certChain == null ? 0 : Arrays.hashCode(certChain)) ^ opensslKey.hashCode(); } }