| /******************************************************************************* |
| * Copyright (c) 2007, 2010 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.osgi.internal.service.security; |
| |
| import java.io.*; |
| import java.security.*; |
| import java.security.cert.*; |
| import java.security.cert.Certificate; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import org.eclipse.osgi.framework.log.FrameworkLogEntry; |
| import org.eclipse.osgi.internal.signedcontent.SignedBundleHook; |
| import org.eclipse.osgi.internal.signedcontent.SignedContentMessages; |
| import org.eclipse.osgi.service.security.TrustEngine; |
| import org.eclipse.osgi.util.NLS; |
| |
| //*potential enhancements* |
| // 1. reloading from the backing file when it changes |
| // 3. methods to support lock/unlock |
| // 3a. Using a callback handler to collect the password |
| // 3b. managing lock/unlock between multiple threads. dealing with SWT UI thread |
| // 4. methods to support changing password, etc |
| // 5. methods to support export, etc |
| // 6. 'friendly-name' generator |
| // 7. Listeners for change events |
| public class KeyStoreTrustEngine extends TrustEngine { |
| |
| private KeyStore keyStore; |
| |
| private final String type; |
| private final String path; |
| private final char[] password; |
| private final String name; |
| |
| /** |
| * Create a new KeyStoreTrustEngine that is backed by a KeyStore |
| * @param path - path to the keystore |
| * @param type - the type of keystore at the path location |
| * @param password - the password required to unlock the keystore |
| */ |
| public KeyStoreTrustEngine(String path, String type, char[] password, String name) { //TODO: This should be a *CallbackHandler* |
| this.path = path; |
| this.type = type; |
| this.password = password; |
| this.name = name; |
| } |
| |
| /** |
| * Return the type |
| * @return type - the type for the KeyStore being managed |
| */ |
| private String getType() { |
| return type; |
| } |
| |
| /** |
| * Return the path |
| * @return - the path for the KeyStore being managed |
| */ |
| private String getPath() { |
| return path; |
| } |
| |
| /** |
| * Return the password |
| * @return password - the password as a char[] |
| */ |
| private char[] getPassword() { |
| return password; |
| } |
| |
| /** |
| * Return the KeyStore managed |
| * @return The KeyStore instance, initialized and loaded |
| * @throws KeyStoreException |
| */ |
| private synchronized KeyStore getKeyStore() throws IOException, GeneralSecurityException { |
| if (null == keyStore) { |
| keyStore = KeyStore.getInstance(getType()); |
| final InputStream in = getInputStream(); |
| try { |
| loadStore(keyStore, in); |
| } finally { |
| try { |
| in.close(); |
| } catch (IOException e) { |
| //ignore secondary failure |
| } |
| } |
| } |
| |
| if (keyStore == null) |
| throw new KeyStoreException(NLS.bind(SignedContentMessages.Default_Trust_Keystore_Load_Failed, getPath())); |
| |
| return keyStore; |
| } |
| |
| public Certificate findTrustAnchor(Certificate[] certChain) throws IOException { |
| |
| if (certChain == null || certChain.length == 0) |
| throw new IllegalArgumentException("Certificate chain is required"); //$NON-NLS-1$ |
| |
| try { |
| Certificate rootCert = null; |
| KeyStore store = getKeyStore(); |
| for (int i = 0; i < certChain.length; i++) { |
| if (certChain[i] instanceof X509Certificate) { |
| if (i == certChain.length - 1) { |
| // this is the last certificate in the chain |
| // determine if we have a valid root |
| X509Certificate cert = (X509Certificate) certChain[i]; |
| if (cert.getSubjectDN().equals(cert.getIssuerDN())) { |
| cert.verify(cert.getPublicKey()); |
| rootCert = cert; // this is a self-signed certificate |
| } else { |
| // try to find a parent, we have an incomplete chain |
| return findAlternativeRoot(cert, store); |
| } |
| } else { |
| X509Certificate nextX509Cert = (X509Certificate) certChain[i + 1]; |
| certChain[i].verify(nextX509Cert.getPublicKey()); |
| } |
| } |
| |
| synchronized (store) { |
| String alias = rootCert == null ? null : store.getCertificateAlias(rootCert); |
| if (alias != null) |
| return store.getCertificate(alias); |
| else if (rootCert != certChain[i]) { |
| alias = store.getCertificateAlias(certChain[i]); |
| if (alias != null) |
| return store.getCertificate(alias); |
| } |
| // if we have reached the end and the last cert is not found to be a valid root CA |
| // then we need to back off the root CA and try to find an alternative |
| if (certChain.length > 1 && i == certChain.length - 1 && certChain[i - 1] instanceof X509Certificate) |
| return findAlternativeRoot((X509Certificate) certChain[i - 1], store); |
| } |
| } |
| } catch (KeyStoreException e) { |
| throw (IOException) new IOException(e.getMessage()).initCause(e); |
| } catch (GeneralSecurityException e) { |
| SignedBundleHook.log(e.getMessage(), FrameworkLogEntry.WARNING, e); |
| return null; |
| } |
| return null; |
| } |
| |
| private Certificate findAlternativeRoot(X509Certificate cert, KeyStore store) throws InvalidKeyException, KeyStoreException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException, CertificateException { |
| synchronized (store) { |
| for (Enumeration e = store.aliases(); e.hasMoreElements();) { |
| Certificate nextCert = store.getCertificate((String) e.nextElement()); |
| if (nextCert instanceof X509Certificate && ((X509Certificate) nextCert).getSubjectDN().equals(cert.getIssuerDN())) { |
| cert.verify(nextCert.getPublicKey()); |
| return nextCert; |
| } |
| } |
| return null; |
| } |
| } |
| |
| protected String doAddTrustAnchor(Certificate cert, String alias) throws IOException, GeneralSecurityException { |
| if (isReadOnly()) |
| throw new IOException(SignedContentMessages.Default_Trust_Read_Only); |
| if (cert == null) { |
| throw new IllegalArgumentException("Certificate must be specified"); //$NON-NLS-1$ |
| } |
| try { |
| KeyStore store = getKeyStore(); |
| synchronized (store) { |
| String oldAlias = store.getCertificateAlias(cert); |
| if (null != oldAlias) |
| throw new CertificateException(SignedContentMessages.Default_Trust_Existing_Cert); |
| Certificate oldCert = store.getCertificate(alias); |
| if (null != oldCert) |
| throw new CertificateException(SignedContentMessages.Default_Trust_Existing_Alias); |
| store.setCertificateEntry(alias, cert); |
| final OutputStream out = getOutputStream(); |
| try { |
| saveStore(store, out); |
| } finally { |
| safeClose(out); |
| } |
| } |
| } catch (KeyStoreException ke) { |
| throw (CertificateException) new CertificateException(ke.getMessage()).initCause(ke); |
| } |
| return alias; |
| } |
| |
| protected void doRemoveTrustAnchor(Certificate cert) throws IOException, GeneralSecurityException { |
| if (isReadOnly()) |
| throw new IOException(SignedContentMessages.Default_Trust_Read_Only); |
| if (cert == null) { |
| throw new IllegalArgumentException("Certificate must be specified"); //$NON-NLS-1$ |
| } |
| try { |
| KeyStore store = getKeyStore(); |
| synchronized (store) { |
| String alias = store.getCertificateAlias(cert); |
| if (alias == null) { |
| throw new CertificateException(SignedContentMessages.Default_Trust_Cert_Not_Found); |
| } |
| removeTrustAnchor(alias); |
| } |
| } catch (KeyStoreException ke) { |
| throw (CertificateException) new CertificateException(ke.getMessage()).initCause(ke); |
| } |
| } |
| |
| protected void doRemoveTrustAnchor(String alias) throws IOException, GeneralSecurityException { |
| |
| if (alias == null) { |
| throw new IllegalArgumentException("Alias must be specified"); //$NON-NLS-1$ |
| } |
| try { |
| KeyStore store = getKeyStore(); |
| synchronized (store) { |
| Certificate oldCert = store.getCertificate(alias); |
| if (oldCert == null) |
| throw new CertificateException(SignedContentMessages.Default_Trust_Cert_Not_Found); |
| store.deleteEntry(alias); |
| final OutputStream out = getOutputStream(); |
| try { |
| saveStore(store, out); |
| } finally { |
| safeClose(out); |
| } |
| } |
| } catch (KeyStoreException ke) { |
| throw (CertificateException) new CertificateException(ke.getMessage()).initCause(ke); |
| } |
| } |
| |
| public Certificate getTrustAnchor(String alias) throws IOException, GeneralSecurityException { |
| |
| if (alias == null) { |
| throw new IllegalArgumentException("Alias must be specified"); //$NON-NLS-1$ |
| } |
| |
| try { |
| KeyStore store = getKeyStore(); |
| synchronized (store) { |
| return store.getCertificate(alias); |
| } |
| } catch (KeyStoreException ke) { |
| throw (CertificateException) new CertificateException(ke.getMessage()).initCause(ke); |
| } |
| } |
| |
| public String[] getAliases() throws IOException, GeneralSecurityException { |
| |
| ArrayList returnList = new ArrayList(); |
| try { |
| KeyStore store = getKeyStore(); |
| synchronized (store) { |
| for (Enumeration aliases = store.aliases(); aliases.hasMoreElements();) { |
| String currentAlias = (String) aliases.nextElement(); |
| if (store.isCertificateEntry(currentAlias)) { |
| returnList.add(currentAlias); |
| } |
| } |
| } |
| } catch (KeyStoreException ke) { |
| throw (CertificateException) new CertificateException(ke.getMessage()).initCause(ke); |
| } |
| return (String[]) returnList.toArray(new String[] {}); |
| } |
| |
| /** |
| * Load using the current password |
| */ |
| private void loadStore(KeyStore store, InputStream is) throws IOException, GeneralSecurityException { |
| store.load(is, getPassword()); |
| } |
| |
| /** |
| * Save using the current password |
| */ |
| private void saveStore(KeyStore store, OutputStream os) throws IOException, GeneralSecurityException { |
| store.store(os, getPassword()); |
| } |
| |
| /** |
| * Closes a stream and ignores any resulting exception. This is useful |
| * when doing stream cleanup in a finally block where secondary exceptions |
| * are not worth logging. |
| */ |
| private void safeClose(OutputStream out) { |
| try { |
| if (out != null) |
| out.close(); |
| } catch (IOException e) { |
| //ignore |
| } |
| } |
| |
| /** |
| * Get an input stream for the KeyStore managed |
| * @return inputstream - the stream |
| * @throws KeyStoreException |
| */ |
| private InputStream getInputStream() throws IOException { |
| return new FileInputStream(new File(getPath())); |
| } |
| |
| /** |
| * Get an output stream for the KeyStore managed |
| * @return outputstream - the stream |
| * @throws KeyStoreException |
| */ |
| private OutputStream getOutputStream() throws IOException { |
| |
| File file = new File(getPath()); |
| if (!file.exists()) |
| file.createNewFile(); |
| |
| return new FileOutputStream(file); |
| } |
| |
| public boolean isReadOnly() { |
| return getPassword() == null || !(new File(path).canWrite()); |
| } |
| |
| public String getName() { |
| return name; |
| } |
| } |