diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/SSLClientUtil.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/SSLClientUtil.java index 73df10472..37063a3af 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/SSLClientUtil.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/SSLClientUtil.java @@ -199,6 +199,7 @@ public class SSLClientUtil { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); fis = new FileInputStream(ks); keyStore.load(fis, ksPass.toCharArray()); + KeyStoreUtil.logCertExpiration(keyStore, ks.getAbsolutePath(), 180*24*60*60*1000L); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keyPass.toCharArray()); sslc.init(kmf.getKeyManagers(), null, I2PAppContext.getGlobalContext().random()); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java index 0a79299e4..c5272144d 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -853,9 +853,12 @@ public class RouterConsoleRunner implements RouterApp { */ private boolean verifyKeyStore(File ks, Set altNames) { if (ks.exists()) { + String ksPW = _context.getProperty(PROP_KEYSTORE_PASSWORD, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD); + KeyStoreUtil.logCertExpiration(ks, ksPW, 180*24*60*60*1000L); boolean rv = _context.getProperty(PROP_KEY_PASSWORD) != null; if (!rv) - System.err.println("Console SSL error, must set " + PROP_KEY_PASSWORD + " in " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath()); + System.err.println("Console SSL error, must set " + PROP_KEY_PASSWORD + " in " + + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath()); return rv; } return createKeyStore(ks, altNames); diff --git a/apps/sam/java/src/net/i2p/sam/SSLUtil.java b/apps/sam/java/src/net/i2p/sam/SSLUtil.java index 496b72d63..e39fc291d 100644 --- a/apps/sam/java/src/net/i2p/sam/SSLUtil.java +++ b/apps/sam/java/src/net/i2p/sam/SSLUtil.java @@ -163,6 +163,7 @@ class SSLUtil { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); fis = new FileInputStream(ks); keyStore.load(fis, ksPass.toCharArray()); + KeyStoreUtil.logCertExpiration(keyStore, ks.getAbsolutePath(), 180*24*60*60*1000L); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keyPass.toCharArray()); sslc.init(kmf.getKeyManagers(), null, I2PAppContext.getGlobalContext().random()); diff --git a/apps/sam/java/src/net/i2p/sam/client/SSLUtil.java b/apps/sam/java/src/net/i2p/sam/client/SSLUtil.java index 8f16eb22e..f24798047 100644 --- a/apps/sam/java/src/net/i2p/sam/client/SSLUtil.java +++ b/apps/sam/java/src/net/i2p/sam/client/SSLUtil.java @@ -164,6 +164,7 @@ class SSLUtil { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); fis = new FileInputStream(ks); keyStore.load(fis, ksPass.toCharArray()); + KeyStoreUtil.logCertExpiration(keyStore, ks.getAbsolutePath(), 180*24*60*60*1000L); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keyPass.toCharArray()); sslc.init(kmf.getKeyManagers(), null, I2PAppContext.getGlobalContext().random()); diff --git a/core/java/src/net/i2p/crypto/KeyStoreUtil.java b/core/java/src/net/i2p/crypto/KeyStoreUtil.java index dc7111e4c..83b55383b 100644 --- a/core/java/src/net/i2p/crypto/KeyStoreUtil.java +++ b/core/java/src/net/i2p/crypto/KeyStoreUtil.java @@ -8,6 +8,7 @@ import java.io.OutputStream; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.PrivateKey; import java.security.PublicKey; @@ -29,6 +30,7 @@ import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.crypto.provider.I2PProvider; import net.i2p.data.Base32; +import net.i2p.data.DataHelper; import net.i2p.util.Log; import net.i2p.util.SecureDirectory; import net.i2p.util.SecureFileOutputStream; @@ -281,6 +283,115 @@ public final class KeyStoreUtil { return count; } + /** + * Validate expiration for all private key certs in a key store. + * Use this for keystores containing selfsigned certs where the + * user will be expected to renew an expiring cert. + * Use this for Jetty keystores, where we aren't doing the loading ourselves. + * + * If a cert isn't valid, it will probably cause bigger problems later when it's used. + * + * @param f keystore file + * @param ksPW keystore password + * @param expiresWithin ms if cert expires within this long, we will log a warning, e.g. 180*24*60*60*1000L + * @return true if all are good, false if we logged something + * @since 0.9.34 + */ + public static boolean logCertExpiration(File f, String ksPW, long expiresWithin) { + String location = f.getAbsolutePath(); + InputStream fis = null; + try { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + fis = new FileInputStream(f); + ks.load(fis, ksPW.toCharArray()); + return logCertExpiration(ks, location, expiresWithin); + } catch (IOException ioe) { + error("Unable to check certificates in key store " + location, ioe); + return false; + } catch (GeneralSecurityException gse) { + error("Unable to check certificates in key store " + location, gse); + return false; + } finally { + try { if (fis != null) fis.close(); } catch (IOException foo) {} + } + } + + /** + * Validate expiration for all private key certs in a key store. + * Use this for keystores containing selfsigned certs where the + * user will be expected to renew an expiring cert. + * Use this for keystores we are feeding to an SSLContext and ServerSocketFactory. + * + * We added support for self-signed certs in 0.8.3 2011-01, with a 10-year expiration. + * We still don't generate them by default. We don't expect anybody's + * certs to expire until 2021. + * + * @param location the path or other identifying info, for logging only + * @param expiresWithin ms if cert expires within this long, we will log a warning, e.g. 180*24*60*60*1000L + * @return true if all are good, false if we logged something + * @since 0.9.34 + */ + public static boolean logCertExpiration(KeyStore ks, String location, long expiresWithin) { + boolean rv = true; + try { + int count = 0; + for(Enumeration e = ks.aliases(); e.hasMoreElements();) { + String alias = e.nextElement(); + if (ks.isKeyEntry(alias)) { + Certificate[] cs; + try { + cs = ks.getCertificateChain(alias); + } catch (KeyStoreException kse) { + error("Unable to check certificates for \"" + alias + "\" in key store " + location, kse); + rv = false; + continue; + } + for (Certificate c : cs) { + if (c != null && (c instanceof X509Certificate)) { + count++; + X509Certificate cert = (X509Certificate) c; + try { + //System.out.println("checking " + alias + " in " + location); + cert.checkValidity(); + long expiresIn = cert.getNotAfter().getTime() - System.currentTimeMillis(); + //System.out.println("expiration of " + alias + " is in " + DataHelper.formatDuration(expiresIn)); + if (expiresIn < expiresWithin) { + Log l = I2PAppContext.getGlobalContext().logManager().getLog(KeyStoreUtil.class); + String subj = cert.getIssuerX500Principal().toString(); + l.logAlways(Log.WARN, "Certificate \"" + subj + "\" in key store " + location + + " will expire in " + DataHelper.formatDuration2(expiresIn).replace(" ", " ") + + "\nYou should renew the certificate soon." + + // TODO better help or tools, or autorenew + "\nFor a local self-signed certificate, you may delete the keystore and restart," + + " or ask for help on how to renew."); + } + } catch (CertificateExpiredException cee) { + String subj = cert.getIssuerX500Principal().toString(); + error("Expired certificate \"" + subj + "\" in key store " + location + + "\nYou must renew the certificate." + + // TODO better help or tools, or autorenew + "\nFor a local self-signed certificate, you may simply delete the keystore and restart," + + "\nor ask for help on how to renew.", + null); + rv = false; + } catch (CertificateNotYetValidException cnyve) { + String subj = cert.getIssuerX500Principal().toString(); + error("Not yet valid certificate \"" + subj + "\" in key store " + location, null); + rv = false; + } + } + } + } + } + if (count == 0) + error("No certificates found in key store " + location, null); + } catch (GeneralSecurityException e) { + error("Unable to check certificates in key store " + location, e); + rv = false; + } + return rv; + } + /** * Remove all blacklisted X509 Certs in a key store. * diff --git a/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java b/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java index 8331e7794..a6400c3ae 100644 --- a/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java +++ b/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java @@ -142,6 +142,7 @@ class SSLClientListenerRunner extends ClientListenerRunner { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); fis = new FileInputStream(ks); keyStore.load(fis, ksPass.toCharArray()); + KeyStoreUtil.logCertExpiration(keyStore, ks.getAbsolutePath(), 180*24*60*60*1000L); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keyPass.toCharArray()); sslc.init(kmf.getKeyManagers(), null, _context.random());