diff --git a/core/java/src/net/i2p/crypto/CertUtil.java b/core/java/src/net/i2p/crypto/CertUtil.java index c443624a1..31a55ad1a 100644 --- a/core/java/src/net/i2p/crypto/CertUtil.java +++ b/core/java/src/net/i2p/crypto/CertUtil.java @@ -156,6 +156,30 @@ public final class CertUtil { throw new IOException("Failed write to " + out); } + /** + * Get the set of Subject Alternative Names, including + * DNSNames, RFC822Names, IPv4 and v6 addresses as strings. + * + * see X509Certificate.getSubjectAlternativeNames() + * + * @return non-null, empty on error or none found + * @since 0.9.34 + */ + public static Set getSubjectAlternativeNames(X509Certificate cert) { + Set rv = new HashSet(8); + try { + Collection> c = cert.getSubjectAlternativeNames(); + if (c != null) { + for (List list : c) { + try { + rv.add((String) list.get(1)); + } catch (ClassCastException cce) {} + } + } + } catch(GeneralSecurityException gse) {} + return rv; + } + /** * Get a value out of the subject distinguished name. * diff --git a/core/java/src/net/i2p/crypto/KeyStoreUtil.java b/core/java/src/net/i2p/crypto/KeyStoreUtil.java index 83b55383b..a040acd3d 100644 --- a/core/java/src/net/i2p/crypto/KeyStoreUtil.java +++ b/core/java/src/net/i2p/crypto/KeyStoreUtil.java @@ -1005,7 +1005,7 @@ public final class KeyStoreUtil { /** * Export the private key and certificate chain (if any) out of a keystore. - * Does NOT close the stream. Throws on all errors. + * Does NOT close the output stream. Throws on all errors. * * @param ks path to the keystore * @param ksPW the keystore password, may be null @@ -1033,6 +1033,59 @@ public final class KeyStoreUtil { } } + /** + * Renew the the private key certificate in a keystore. + * Closes the input and output streams. Throws on all errors. + * + * @param ks path to the keystore + * @param ksPW the keystore password, may be null + * @param alias the name of the key, or null to get the first one in keystore + * @param keyPW the key password, must be at least 6 characters + * @param validDays new cert to expire this many days from now + * @return the new certificate + * @since 0.9.34 + */ + public static X509Certificate renewPrivateKeyCertificate(File ks, String ksPW, String alias, + String keyPW, int validDays) + throws GeneralSecurityException, IOException { + InputStream fis = null; + OutputStream fos = null; + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + fis = new FileInputStream(ks); + char[] pwchars = ksPW != null ? ksPW.toCharArray() : null; + keyStore.load(fis, pwchars); + try { fis.close(); } catch (IOException ioe) {} + fis = null; + char[] keypwchars = keyPW.toCharArray(); + if (alias == null) { + for (Enumeration e = keyStore.aliases(); e.hasMoreElements();) { + alias = e.nextElement(); + break; + } + if (alias == null) + throw new GeneralSecurityException("no private keys found"); + } + PrivateKey pk = (PrivateKey) keyStore.getKey(alias, keypwchars); + if (pk == null) + throw new GeneralSecurityException("private key not found: " + alias); + Certificate[] certs = keyStore.getCertificateChain(alias); + if (certs.length != 1) + throw new GeneralSecurityException("Bad cert chain length"); + X509Certificate cert = (X509Certificate) certs[0]; + Object[] rv = SelfSignedGenerator.renew(cert, pk, validDays); + cert = (X509Certificate) rv[2]; + certs[0] = cert; + keyStore.setKeyEntry(alias, pk, keypwchars, certs); + fos = new SecureFileOutputStream(ks); + keyStore.store(fos, pwchars); + return cert; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + /** * Import the private key and certificate chain to a keystore. * Keystore will be created if it does not exist. diff --git a/core/java/src/net/i2p/crypto/SelfSignedGenerator.java b/core/java/src/net/i2p/crypto/SelfSignedGenerator.java index 3a3b02a6d..47982ae71 100644 --- a/core/java/src/net/i2p/crypto/SelfSignedGenerator.java +++ b/core/java/src/net/i2p/crypto/SelfSignedGenerator.java @@ -141,7 +141,30 @@ public final class SelfSignedGenerator { SigningPrivateKey priv = (SigningPrivateKey) keys[1]; PublicKey jpub = SigUtil.toJavaKey(pub); PrivateKey jpriv = SigUtil.toJavaKey(priv); + return generate(jpub, jpriv, priv, type, cname, altNames, ou, o, l, st, c, validDays); + } + /** + * @param cname the common name, non-null. Must be a hostname or email address. IP addresses will not be correctly encoded. + * @param altNames the Subject Alternative Names. May be null. May contain hostnames and/or IP addresses. + * cname, localhost, 127.0.0.1, and ::1 will be automatically added. + * @param ou The OU (organizational unit) in the distinguished name, non-null before 0.9.28, may be null as of 0.9.28 + * @param o The O (organization)in the distinguished name, non-null before 0.9.28, may be null as of 0.9.28 + * @param l The L (city or locality) in the distinguished name, non-null before 0.9.28, may be null as of 0.9.28 + * @param st The ST (state or province) in the distinguished name, non-null before 0.9.28, may be null as of 0.9.28 + * @param c The C (country) in the distinguished name, non-null before 0.9.28, may be null as of 0.9.28 + * + * @return length 4 array: + * rv[0] is a Java PublicKey + * rv[1] is a Java PrivateKey + * rv[2] is a Java X509Certificate + * rv[3] is a Java X509CRL + * + * @since 0.9.34 added altNames param + */ + private static Object[] generate(PublicKey jpub, PrivateKey jpriv, SigningPrivateKey priv, SigType type, + String cname, Set altNames, String ou, String o, String l, String st, String c, + int validDays) throws GeneralSecurityException { String oid; switch (type) { case DSA_SHA1: @@ -222,6 +245,37 @@ public final class SelfSignedGenerator { return rv; } + /** + * @param cert the old cert to be replaced + * @param jpriv the private key + * + * @return length 4 array: + * rv[0] is a Java PublicKey, from cert as passed in + * rv[1] is a Java PrivateKey, jpriv as passed in + * rv[2] is a Java X509Certificate, new one + * rv[3] is a Java X509CRL, new one + * + * @since 0.9.34 added altNames param + */ + public static Object[] renew(X509Certificate cert, PrivateKey jpriv, int validDays) throws GeneralSecurityException { + String cname = CertUtil.getSubjectValue(cert, "CN"); + if (cname == null) + cname = "localhost"; + String ou = CertUtil.getSubjectValue(cert, "OU"); + String o = CertUtil.getSubjectValue(cert, "O"); + String l = CertUtil.getSubjectValue(cert, "L"); + String st = CertUtil.getSubjectValue(cert, "ST"); + String c = CertUtil.getSubjectValue(cert, "C"); + Set altNames = CertUtil.getSubjectAlternativeNames(cert); + SigningPrivateKey priv = SigUtil.fromJavaKey(jpriv); + SigType type = priv.getType(); + SigningPublicKey pub = KeyGenerator.getSigningPublicKey(priv); + PublicKey jpub = SigUtil.toJavaKey(pub); + if (type == null) + throw new GeneralSecurityException("Unsupported: " + jpriv); + return generate(jpub, jpriv, priv, type, cname, altNames, ou, o, l, st, c, validDays); + } + /** * Generate a CRL for the given cert, signed with the given private key */ @@ -833,8 +887,54 @@ public final class SelfSignedGenerator { /** * Note: For CLI testing, use java -jar i2p.jar su3file keygen pubkey.crt keystore.ks commonName */ + public static void main(String[] args) throws Exception { + if (args.length == 0) { + usage(); + } else if (args[0].equals("keygen")) { + if (args.length >= 4) + SU3File.main(args); + else + usage(); + } else if (args[0].equals("renew")) { + if (args.length >= 3) { + String ksPW, cert, ks; + if (args[1].equals("-p")) { + ksPW = args[2]; + cert = args[3]; + ks = args[4]; + } else { + ksPW = KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD; + cert = args[1]; + ks = args[2]; + } + String keypw = ""; + try { + while (keypw.length() < 6) { + System.out.print("Enter password for key: "); + keypw = DataHelper.readLine(System.in); + if (keypw == null) { + System.out.println("\nEOF reading password"); + System.exit(1); + } + keypw = keypw.trim(); + if (keypw.length() > 0 && keypw.length() < 6) + System.out.println("Key password must be at least 6 characters"); + } + } catch (IOException ioe) { + System.out.println("Error asking for password"); + throw ioe; + } + File ksf = new File(ks); + X509Certificate newCert = KeyStoreUtil.renewPrivateKeyCertificate(ksf, ksPW, null, keypw, 3652); + CertUtil.saveCert(newCert, new File(cert)); + System.out.println("Certificate renewed for 10 years, and stored in " + cert + " and " + ks); + } else { + usage(); + } + } else { + usage(); + } /**** - public static void main(String[] args) { try { int i = 0; for (SigType t : java.util.EnumSet.allOf(SigType.class)) { @@ -845,8 +945,14 @@ public final class SelfSignedGenerator { } catch (Exception e) { e.printStackTrace(); } +****/ } + private static void usage() { + System.err.println("Usage: selfsignedgenerator keygen [-t type|code] [-p keystorepw] [-r crlFile.crl] publicKeyFile.crt keystore.ks localhost\n" + + " selfsignedgenerator renew [-p keystorepw] publicKeyFile.crt keystore.ks"); + } +/**** private static final void test(String name, SigType type) throws Exception { Object[] rv = generate("cname@example.com", "ou", "o", null, "st", "c", 3652, type); //PublicKey jpub = (PublicKey) rv[0]; diff --git a/core/java/src/net/i2p/util/CommandLine.java b/core/java/src/net/i2p/util/CommandLine.java index 66172a0db..3e857e49c 100644 --- a/core/java/src/net/i2p/util/CommandLine.java +++ b/core/java/src/net/i2p/util/CommandLine.java @@ -23,6 +23,7 @@ public class CommandLine { "net.i2p.CoreVersion", "net.i2p.crypto.CertUtil", "net.i2p.crypto.CryptoCheck", + "net.i2p.crypto.SelfSignedGenerator", "net.i2p.crypto.SU3File", "net.i2p.crypto.TrustedUpdate", "net.i2p.data.Base32",