package net.i2p.crypto;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertStore;
import java.security.cert.X509Certificate;
import java.security.cert.X509CRL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import net.i2p.I2PAppContext;
import net.i2p.crypto.provider.I2PProvider;
import net.i2p.data.Base32;
import net.i2p.util.Log;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.ShellCommand;
import net.i2p.util.SystemVersion;
/**
* Keystore utilities, consolidated from various places.
*
* @since 0.9.9
*/
public final class KeyStoreUtil {
public static boolean _blacklistLogged;
public static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
private static final String DEFAULT_KEY_ALGORITHM = "RSA";
private static final int DEFAULT_KEY_SIZE = 2048;
private static final int DEFAULT_KEY_VALID_DAYS = 3652; // 10 years
static {
I2PProvider.addProvider();
}
/**
* SHA1 hashes
*
* No reports of some of these in a Java keystore but just to be safe...
* CNNIC ones are in Ubuntu keystore.
*
* In comments below are the serial numer, CN, and OU
*/
private static final String[] BLACKLIST_SHA1 = new String[] {
// CNNIC https://googleonlinesecurity.blogspot.com/2015/03/maintaining-digital-certificate-security.html
//new BigInteger("49:33:00:01".replace(":", ""), 16),
//"CNNIC ROOT",
//null,
"8b:af:4c:9b:1d:f0:2a:92:f7:da:12:8e:b9:1b:ac:f4:98:60:4b:6f",
// CNNIC EV root https://bugzilla.mozilla.org/show_bug.cgi?id=607208
//new BigInteger("48:9f:00:01".replace(":", ""), 16),
//"China Internet Network Information Center EV Certificates Root",
//null,
"4f:99:aa:93:fb:2b:d1:37:26:a1:99:4a:ce:7f:f0:05:f2:93:5d:1e",
// Superfish http://blog.erratasec.com/2015/02/extracting-superfish-certificate.html
//new BigInteger("d2:fc:13:87:a9:44:dc:e7".replace(":", ""), 16),
//"Superfish, Inc.",
//null,
"c8:64:48:48:69:d4:1d:2b:0d:32:31:9c:5a:62:f9:31:5a:af:2c:bd",
// eDellRoot https://www.reddit.com/r/technology/comments/3twmfv/dell_ships_laptops_with_rogue_root_ca_exactly/
//new BigInteger("6b:c5:7b:95:18:93:aa:97:4b:62:4a:c0:88:fc:3b:b6".replace(":", ""), 16),
//"eDellRoot",
//null,
"98:a0:4e:41:63:35:77:90:c4:a7:9e:6d:71:3f:f0:af:51:fe:69:27",
// DSDTestProvider https://blog.hboeck.de/archives/876-Superfish-2.0-Dangerous-Certificate-on-Dell-Laptops-breaks-encrypted-HTTPS-Connections.html
// serial number is actually negative; hex string as reported by certtool below
//new BigInteger("a4:4c:38:47:f8:ee:71:80:43:4d:b1:80:b9:a7:e9:62".replace(":", ""), 16)
//new BigInteger("-5b:b3:c7:b8:07:11:8e:7f:bc:b2:4e:7f:46:58:16:9e".replace(":", ""), 16),
//"DSDTestProvider",
//null,
"02:c2:d9:31:06:2d:7b:1d:c2:a5:c7:f5:f0:68:50:64:08:1f:b2:21",
// Verisign G1 Roots
// https://googleonlinesecurity.blogspot.com/2015/12/proactive-measures-in-digital.html
// https://knowledge.symantec.com/support/ssl-certificates-support/index?page=content&id=ALERT1941
// SHA-1
//new BigInteger("3c:91:31:cb:1f:f6:d0:1b:0e:9a:b8:d0:44:bf:12:be".replace(":", ""), 16),
//null,
//"Class 3 Public Primary Certification Authority",
"a1:db:63:93:91:6f:17:e4:18:55:09:40:04:15:c7:02:40:b0:ae:6b",
// MD2
//new BigInteger("70:ba:e4:1d:10:d9:29:34:b6:38:ca:7b:03:cc:ba:bf".replace(":", ""), 16),
//null,
//"Class 3 Public Primary Certification Authority",
"74:2c:31:92:e6:07:e4:24:eb:45:49:54:2b:e1:bb:c5:3e:61:74:e2",
// Comodo SHA1 https://cabforum.org/pipermail/public/2015-December/006500.html
// https://bugzilla.mozilla.org/show_bug.cgi?id=1208461
//new BigInteger("44:be:0c:8b:50:00:21:b4:11:d3:2a:68:06:a9:ad:69".replace(":", ""), 16)
//"UTN - DATACorp SGC"
//null
"58:11:9f:0e:12:82:87:ea:50:fd:d9:87:45:6f:4f:78:dc:fa:d6:d4"
};
private static final Set<SHA1Hash> _blacklist = new HashSet<SHA1Hash>(16);
static {
for (int i = 0; i < BLACKLIST_SHA1.length; i++) {
String s = BLACKLIST_SHA1[i].replace(":", "");
BigInteger bi = new BigInteger(s, 16);
byte[] b = bi.toByteArray();
if (b.length == 21) {
byte[] b2 = new byte[20];
System.arraycopy(b, 1, b2, 0, 20);
b = b2;
}
SHA1Hash h = new SHA1Hash(b);
_blacklist.add(h);
}
}
/**
* Create a new KeyStore object, and load it from ksFile if it is
* non-null and it exists.
* If ksFile is non-null and it does not exist, create a new empty
* keystore file.
*
* @param ksFile may be null
* @param password may be null
* @return success
*/
public static KeyStore createKeyStore(File ksFile, String password)
throws GeneralSecurityException, IOException {
boolean exists = ksFile != null && ksFile.exists();
char[] pwchars = password != null ? password.toCharArray() : null;
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
if (exists) {
InputStream fis = null;
try {
fis = new FileInputStream(ksFile);
ks.load(fis, pwchars);
} finally {
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
}
}
if (ksFile != null && !exists) {
OutputStream fos = null;
try {
// must be initted
ks.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
fos = new SecureFileOutputStream(ksFile);
ks.store(fos, pwchars);
} finally {
if (fos != null) try { fos.close(); } catch (IOException ioe) {}
}
}
return ks;
}
/**
* Loads certs from location of javax.net.ssl.keyStore property,
* else from $JAVA_HOME/lib/security/jssecacerts,
* else from $JAVA_HOME/lib/security/cacerts.
*
* @return null on catastrophic failure, returns empty KeyStore if can't load system file
* @since 0.8.2, moved from SSLEepGet.initSSLContext() in 0.9.9
*/
public static KeyStore loadSystemKeyStore() {
KeyStore ks;
try {
ks = KeyStore.getInstance(KeyStore.getDefaultType());
} catch (GeneralSecurityException gse) {
error("Key Store init error", gse);
return null;
}
boolean success = false;
String override = System.getProperty("javax.net.ssl.keyStore");
if (override != null)
success = loadCerts(new File(override), ks);
if (!success) {
if (SystemVersion.isAndroid()) {
if (SystemVersion.getAndroidVersion() >= 14) {
try {
ks.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
success = addCerts(new File(System.getProperty("java.home"), "etc/security/cacerts"), ks) > 0;
} catch (IOException e) {
} catch (GeneralSecurityException e) {}
} else {
success = loadCerts(new File(System.getProperty("java.home"), "etc/security/cacerts.bks"), ks);
}
} else {
success = loadCerts(new File(System.getProperty("java.home"), "lib/security/jssecacerts"), ks);
if (!success)
success = loadCerts(new File(System.getProperty("java.home"), "lib/security/cacerts"), ks);
}
}
if (success) {
removeBlacklistedCerts(ks);
} else {
try {
// must be initted
ks.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
} catch (IOException e) {
} catch (GeneralSecurityException e) {}
error("All key store loads failed, will only load local certificates", null);
}
return ks;
}
/**
* Load all X509 Certs from a key store File into a KeyStore
* Note that each call reinitializes the KeyStore
*
* @return success
* @since 0.8.2, moved from SSLEepGet in 0.9.9
*/
private static boolean loadCerts(File file, KeyStore ks) {
if (!file.exists())
return false;
InputStream fis = null;
try {
fis = new FileInputStream(file);
// "changeit" is the default password
ks.load(fis, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
info("Certs loaded from " + file);
} catch (GeneralSecurityException gse) {
error("KeyStore load error, no default keys: " + file.getAbsolutePath(), gse);
try {
// not clear if null is allowed for password
ks.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
} catch (IOException foo) {
} catch (GeneralSecurityException e) {}
return false;
} catch (IOException ioe) {
error("KeyStore load error, no default keys: " + file.getAbsolutePath(), ioe);
try {
ks.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
} catch (IOException foo) {
} catch (GeneralSecurityException e) {}
return false;
} finally {
try { if (fis != null) fis.close(); } catch (IOException foo) {}
}
return true;
}
/**
* Count all X509 Certs in a key store
*
* @return number successfully added
* @since 0.8.2, moved from SSLEepGet in 0.9.9
*/
public static int countCerts(KeyStore ks) {
int count = 0;
try {
for(Enumeration<String> e = ks.aliases(); e.hasMoreElements();) {
String alias = e.nextElement();
if (ks.isCertificateEntry(alias)) {
//info("Found cert " + alias);
count++;
}
}
} catch (GeneralSecurityException e) {}
return count;
}
/**
* Remove all blacklisted X509 Certs in a key store.
*
* @return number successfully removed
* @since 0.9.24
*/
private static int removeBlacklistedCerts(KeyStore ks) {
if (SystemVersion.isAndroid())
return 0;
int count = 0;
try {
MessageDigest md = SHA1.getInstance();
for(Enumeration<String> e = ks.aliases(); e.hasMoreElements();) {
String alias = e.nextElement();
if (ks.isCertificateEntry(alias)) {
Certificate c = ks.getCertificate(alias);
if (c != null && (c instanceof X509Certificate)) {
//X509Certificate xc = (X509Certificate) c;
//BigInteger serial = xc.getSerialNumber();
// debug:
//String xname = CertUtil.getIssuerValue(xc, "CN");
//info("Found \"" + xname + "\" s/n: " + serial.toString(16));
//if (xname == null)
// info("name is null, full issuer: " + xc.getIssuerX500Principal().getName());
byte[] enc = c.getEncoded();
if (enc != null) {
byte[] h = md.digest(enc);
//StringBuilder buf = new StringBuilder(60);
//String hex = DataHelper.toString(h);
//for (int i = 0; i < hex.length(); i += 2) {
// buf.append(hex.charAt(i));
// buf.append(hex.charAt(i+1));
// if (i < hex.length() - 2)
// buf.append(':');
//}
//info("hex is: " + buf);
if (_blacklist.contains(new SHA1Hash(h))) {
ks.deleteEntry(alias);
count++;
if (!_blacklistLogged) {
// should this be a logAlways?
X509Certificate xc = (X509Certificate) c;
BigInteger serial = xc.getSerialNumber();
String cn = CertUtil.getIssuerValue(xc, "CN");
String ou = CertUtil.getIssuerValue(xc, "OU");
warn("Ignoring blacklisted certificate \"" + alias +
"\" CN: \"" + cn +
"\" OU: \"" + ou +
"\" s/n: " + serial.toString(16), null);
}
}
} else {
info("null encoding!!!");
}
}
}
}
} catch (GeneralSecurityException e) {}
if (count > 0)
_blacklistLogged = true;
return count;
}
/**
* Load all X509 Certs from a directory and add them to the
* trusted set of certificates in the key store
*
* This DOES check for revocation.
*
* @return number successfully added
* @since 0.8.2, moved from SSLEepGet in 0.9.9
*/
public static int addCerts(File dir, KeyStore ks) {
info("Looking for X509 Certificates in " + dir.getAbsolutePath());
int added = 0;
if (dir.exists() && dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null) {
CertStore cs = CertUtil.loadCRLs();
for (int i = 0; i < files.length; i++) {
File f = files[i];
if (!f.isFile())
continue;
// use file name as alias
// https://www.sslshopper.com/ssl-converter.html
// No idea if all these formats can actually be read by CertificateFactory
String alias = f.getName().toLowerCase(Locale.US);
if (alias.endsWith(".crt") || alias.endsWith(".pem") || alias.endsWith(".key") ||
alias.endsWith(".der") || alias.endsWith(".key") || alias.endsWith(".p7b") ||
alias.endsWith(".p7c") || alias.endsWith(".pfx") || alias.endsWith(".p12") ||
alias.endsWith(".cer"))
alias = alias.substring(0, alias.length() - 4);
boolean success = addCert(f, alias, ks, cs);
if (success)
added++;
}
}
}
return added;
}
/**
* Load an X509 Cert from a file and add it to the
* trusted set of certificates in the key store
*
* This does NOT check for revocation.
*
* @return success
* @since 0.8.2, moved from SSLEepGet in 0.9.9
*/
public static boolean addCert(File file, String alias, KeyStore ks) {
return addCert(file, alias, ks, null);
}
/**
* Load an X509 Cert from a file and add it to the
* trusted set of certificates in the key store
*
* This DOES check for revocation, IF cs is non-null.
*
* @param cs may be null; if non-null, check for revocation
* @return success
* @since 0.9.25
*/
public static boolean addCert(File file, String alias, KeyStore ks, CertStore cs) {
try {
X509Certificate cert = CertUtil.loadCert(file);
info("Read X509 Certificate from " + file.getAbsolutePath() +
" Issuer: " + cert.getIssuerX500Principal() +
" Serial: " + cert.getSerialNumber().toString(16) +
"; Valid From: " + cert.getNotBefore() +
" To: " + cert.getNotAfter());
if (cs != null && CertUtil.isRevoked(cs, cert)) {
error("Certificate is revoked: " + file, new Exception());
return false;
}
ks.setCertificateEntry(alias, cert);
info("Now trusting X509 Certificate, Issuer: " + cert.getIssuerX500Principal());
} catch (CertificateExpiredException cee) {
String s = "Rejecting expired X509 Certificate: " + file.getAbsolutePath();
// Android often has old system certs
// our SSL certs may be old also
//if (SystemVersion.isAndroid())
warn(s, cee);
//else
// error(s, cee);
return false;
} catch (CertificateNotYetValidException cnyve) {
error("Rejecting X509 Certificate not yet valid: " + file.getAbsolutePath(), cnyve);
return false;
} catch (GeneralSecurityException gse) {
error("Error reading X509 Certificate: " + file.getAbsolutePath(), gse);
return false;
} catch (IOException ioe) {
error("Error reading X509 Certificate: " + file.getAbsolutePath(), ioe);
return false;
}
return true;
}
/** 48 char b32 string (30 bytes of entropy) */
public static String randomString() {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
// make a random 48 character password (30 * 8 / 5)
byte[] rand = new byte[30];
ctx.random().nextBytes(rand);
return Base32.encode(rand);
}
/**
* Create a keypair and store it in the keystore at ks, creating it if necessary.
* Use default keystore password, valid days, algorithm, and key size.
*
* Warning, may take a long time.
*
* @param ks path to the keystore
* @param alias the name of the key
* @param cname e.g. randomstuff.console.i2p.net
* @param ou e.g. console
* @param keyPW the key password, must be at least 6 characters
*
* @return success
* @since 0.8.3, consolidated from RouterConsoleRunner and SSLClientListenerRunner in 0.9.9
*/
public static boolean createKeys(File ks, String alias, String cname, String ou,
String keyPW) {
return createKeys(ks, DEFAULT_KEYSTORE_PASSWORD, alias, cname, ou,
DEFAULT_KEY_VALID_DAYS, DEFAULT_KEY_ALGORITHM, DEFAULT_KEY_SIZE, keyPW);
}
/**
* Create a keypair and store it in the keystore at ks, creating it if necessary.
*
* For new code, the createKeysAndCRL() with the SigType argument is recommended over this one,
* as it throws exceptions, and returns the certificate and CRL.
*
* Warning, may take a long time.
*
* @param ks path to the keystore
* @param ksPW the keystore password
* @param alias the name of the key
* @param cname e.g. randomstuff.console.i2p.net
* @param ou e.g. console
* @param validDays e.g. 3652 (10 years)
* @param keyAlg e.g. DSA , RSA, EC
* @param keySize e.g. 1024
* @param keyPW the key password, must be at least 6 characters
*
* @return success
* @since 0.8.3, consolidated from RouterConsoleRunner and SSLClientListenerRunner in 0.9.9
*/
public static boolean createKeys(File ks, String ksPW, String alias, String cname, String ou,
int validDays, String keyAlg, int keySize, String keyPW) {
boolean useKeytool = I2PAppContext.getGlobalContext().getBooleanProperty("crypto.useExternalKeytool");
if (useKeytool) {
return createKeysCLI(ks, ksPW, alias, cname, ou, validDays, keyAlg, keySize, keyPW);
} else {
try {
createKeysAndCRL(ks, ksPW, alias, cname, ou, validDays, keyAlg, keySize, keyPW);
return true;
} catch (GeneralSecurityException gse) {
error("Create keys error", gse);
return false;
} catch (IOException ioe) {
error("Create keys error", ioe);
return false;
}
}
}
/**
* New way - Native Java, does not call out to keytool.
* Create a keypair and store it in the keystore at ks, creating it if necessary.
*
* This returns the public key, private key, certificate, and CRL in an array.
* All of these are Java classes. Keys may be converted to I2P classes with SigUtil.
* The private key and selfsigned cert are stored in the keystore.
* The public key may be derived from the private key with KeyGenerator.getSigningPublicKey().
* The public key certificate may be stored separately with
* CertUtil.saveCert() if desired.
* The CRL is not stored by this method, store it with
* CertUtil.saveCRL() or CertUtil.exportCRL() if desired.
*
* Throws on all errors.
* Warning, may take a long time.
*
* @param ks path to the keystore
* @param ksPW the keystore password
* @param alias the name of the key
* @param cname e.g. randomstuff.console.i2p.net
* @param ou e.g. console
* @param validDays e.g. 3652 (10 years)
* @param keyAlg e.g. DSA , RSA, EC
* @param keySize e.g. 1024
* @param keyPW the key password, must be at least 6 characters
* @return all you need:
* 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.25
*/
public static Object[] createKeysAndCRL(File ks, String ksPW, String alias, String cname, String ou,
int validDays, String keyAlg, int keySize, String keyPW)
throws GeneralSecurityException, IOException {
String algoName = getSigAlg(keySize, keyAlg);
SigType type = null;
for (SigType t : EnumSet.allOf(SigType.class)) {
if (t.getAlgorithmName().equals(algoName)) {
type = t;
break;
}
}
if (type == null)
throw new GeneralSecurityException("Unsupported algorithm/size: " + keyAlg + '/' + keySize);
return createKeysAndCRL(ks, ksPW, alias, cname, ou, validDays, type, keyPW);
}
/**
* New way - Native Java, does not call out to keytool.
* Create a keypair and store it in the keystore at ks, creating it if necessary.
*
* This returns the public key, private key, certificate, and CRL in an array.
* All of these are Java classes. Keys may be converted to I2P classes with SigUtil.
* The private key and selfsigned cert are stored in the keystore.
* The public key may be derived from the private key with KeyGenerator.getSigningPublicKey().
* The public key certificate may be stored separately with
* CertUtil.saveCert() if desired.
* The CRL is not stored by this method, store it with
* CertUtil.saveCRL() or CertUtil.exportCRL() if desired.
*
* Throws on all errors.
* Warning, may take a long time.
*
* @param ks path to the keystore
* @param ksPW the keystore password
* @param alias the name of the key
* @param cname e.g. randomstuff.console.i2p.net
* @param ou e.g. console
* @param validDays e.g. 3652 (10 years)
* @param keyPW the key password, must be at least 6 characters
* @return all you need:
* 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.25
*/
public static Object[] createKeysAndCRL(File ks, String ksPW, String alias, String cname, String ou,
int validDays, SigType type, String keyPW)
throws GeneralSecurityException, IOException {
File dir = ks.getParentFile();
if (dir != null && !dir.exists()) {
File sdir = new SecureDirectory(dir.getAbsolutePath());
if (!sdir.mkdirs())
throw new IOException("Can't create directory " + dir);
}
Object[] rv = SelfSignedGenerator.generate(cname, ou, null, "I2P Anonymous Network", null, null, validDays, type);
PublicKey jpub = (PublicKey) rv[0];
PrivateKey jpriv = (PrivateKey) rv[1];
X509Certificate cert = (X509Certificate) rv[2];
X509CRL crl = (X509CRL) rv[3];
List<X509Certificate> certs = Collections.singletonList(cert);
storePrivateKey(ks, ksPW, alias, keyPW, jpriv, certs);
return rv;
}
/**
* OLD way - keytool
* Create a keypair and store it in the keystore at ks, creating it if necessary.
*
* Warning, may take a long time.
*
* @param ks path to the keystore
* @param ksPW the keystore password
* @param alias the name of the key
* @param cname e.g. randomstuff.console.i2p.net
* @param ou e.g. console
* @param validDays e.g. 3652 (10 years)
* @param keyAlg e.g. DSA , RSA, EC
* @param keySize e.g. 1024
* @param keyPW the key password, must be at least 6 characters
*
* @return success
* @since 0.8.3, consolidated from RouterConsoleRunner and SSLClientListenerRunner in 0.9.9
*/
private static boolean createKeysCLI(File ks, String ksPW, String alias, String cname, String ou,
int validDays, String keyAlg, int keySize, String keyPW) {
if (ks.exists()) {
try {
if (getCert(ks, ksPW, alias) != null) {
error("Not overwriting key " + alias + ", already exists in " + ks, null);
return false;
}
} catch (IOException e) {
error("Not overwriting key \"" + alias + "\", already exists in " + ks, e);
return false;
} catch (GeneralSecurityException e) {
error("Not overwriting key \"" + alias + "\", already exists in " + ks, e);
return false;
}
} else {
File dir = ks.getParentFile();
if (dir != null && !dir.exists()) {
File sdir = new SecureDirectory(dir.getAbsolutePath());
if (!sdir.mkdir()) {
error("Can't create directory " + dir, null);
return false;
}
}
}
String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath();
List<String> a = new ArrayList<String>(32);
a.add(keytool);
a.add("-genkey"); // -genkeypair preferred in newer keytools, but this works with more
//a.add("-v"); // verbose, gives you a stack trace on exception
a.add("-storetype"); a.add(KeyStore.getDefaultType());
a.add("-keystore"); a.add(ks.getAbsolutePath());
a.add("-storepass"); a.add(ksPW);
a.add("-alias"); a.add(alias);
a.add("-dname"); a.add("CN=" + cname + ",OU=" + ou + ",O=I2P Anonymous Network,L=XX,ST=XX,C=XX");
a.add("-validity"); a.add(Integer.toString(validDays)); // 10 years
a.add("-keyalg"); a.add(keyAlg);
a.add("-sigalg"); a.add(getSigAlg(keySize, keyAlg));
a.add("-keysize"); a.add(Integer.toString(keySize));
a.add("-keypass"); a.add(keyPW);
if (keyAlg.equals("Ed") || keyAlg.equals("EdDSA") || keyAlg.equals("ElGamal")) {
File f = I2PAppContext.getGlobalContext().getBaseDir();
f = new File(f, "lib");
f = new File(f, "i2p.jar");
// providerpath is not in the man page; see keytool -genkey -help
a.add("-providerpath"); a.add(f.getAbsolutePath());
a.add("-providerclass"); a.add("net.i2p.crypto.provider.I2PProvider");
}
String[] args = a.toArray(new String[a.size()]);
// TODO pipe key password to process; requires ShellCommand enhancements
boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 240);
if (success) {
success = ks.exists();
if (success) {
try {
success = getPrivateKey(ks, ksPW, alias, keyPW) != null;
if (!success)
error("Key gen failed to get private key", null);
} catch (IOException e) {
error("Key gen failed to get private key", e);
success = false;
} catch (GeneralSecurityException e) {
error("Key gen failed to get private key", e);
success = false;
}
}
if (!success)
error("Key gen failed for unknown reasons", null);
}
if (success) {
SecureFileOutputStream.setPerms(ks);
info("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath());
} else {
StringBuilder buf = new StringBuilder(256);
for (int i = 0; i < args.length; i++) {
buf.append('"').append(args[i]).append("\" ");
}
error("Failed to generate keys using command line: " + buf, null);
}
return success;
}
private static String getSigAlg(int size, String keyalg) {
if (keyalg.equals("EC"))
keyalg = "ECDSA";
else if (keyalg.equals("Ed"))
keyalg = "EdDSA";
String hash;
if (keyalg.equals("ECDSA")) {
if (size <= 256)
hash = "SHA256";
else if (size <= 384)
hash = "SHA384";
else
hash = "SHA512";
} else if (keyalg.equals("EdDSA")) {
hash = "SHA512";
} else {
if (size <= 1024)
hash = "SHA1";
else if (size <= 2048)
hash = "SHA256";
else if (size <= 3072)
hash = "SHA384";
else
hash = "SHA512";
}
return hash + "with" + keyalg;
}
/**
* Get a private key out of a keystore
*
* @param ks path to the keystore
* @param ksPW the keystore password, may be null
* @param alias the name of the key
* @param keyPW the key password, must be at least 6 characters
* @return the key or null if not found
*/
public static PrivateKey getPrivateKey(File ks, String ksPW, String alias, String keyPW)
throws GeneralSecurityException, IOException {
InputStream fis = null;
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
fis = new FileInputStream(ks);
char[] pwchars = ksPW != null ? ksPW.toCharArray() : null;
keyStore.load(fis, pwchars);
char[] keypwchars = keyPW.toCharArray();
return (PrivateKey) keyStore.getKey(alias, keypwchars);
} finally {
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
}
}
/**
* Export the private key and certificate chain (if any) out of a keystore.
* Does NOT close the stream. 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
* @param keyPW the key password, must be at least 6 characters
* @since 0.9.25
*/
public static void exportPrivateKey(File ks, String ksPW, String alias, String keyPW,
OutputStream out)
throws GeneralSecurityException, IOException {
InputStream fis = null;
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
fis = new FileInputStream(ks);
char[] pwchars = ksPW != null ? ksPW.toCharArray() : null;
keyStore.load(fis, pwchars);
char[] keypwchars = keyPW.toCharArray();
PrivateKey pk = (PrivateKey) keyStore.getKey(alias, keypwchars);
if (pk == null)
throw new GeneralSecurityException("private key not found: " + alias);
Certificate[] certs = keyStore.getCertificateChain(alias);
CertUtil.exportPrivateKey(pk, certs, out);
} finally {
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
}
}
/**
* Import the private key and certificate chain to a keystore.
* Keystore will be created if it does not exist.
* Private key MUST be first in the stream.
* Closes the stream. 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. If null, will be taken from the Subject CN
* of the first certificate in the chain.
* @param keyPW the key password, must be at least 6 characters
* @return the alias as specified or extracted
* @since 0.9.25
*/
public static String importPrivateKey(File ks, String ksPW, String alias, String keyPW,
InputStream in)
throws GeneralSecurityException, IOException {
OutputStream fos = null;
try {
KeyStore keyStore = createKeyStore(ks, ksPW);
PrivateKey pk = CertUtil.loadPrivateKey(in);
List<X509Certificate> certs = CertUtil.loadCerts(in);
if (alias == null) {
alias = CertUtil.getSubjectValue(certs.get(0), "CN");
if (alias == null)
throw new GeneralSecurityException("no alias specified and no Subject CN in cert");
if (alias.endsWith(".family.i2p.net") && alias.length() > ".family.i2p.net".length())
alias = alias.substring(0, ".family.i2p.net".length());
}
keyStore.setKeyEntry(alias, pk, keyPW.toCharArray(), certs.toArray(new Certificate[certs.size()]));
char[] pwchars = ksPW != null ? ksPW.toCharArray() : null;
fos = new SecureFileOutputStream(ks);
keyStore.store(fos, pwchars);
return alias;
} finally {
if (fos != null) try { fos.close(); } catch (IOException ioe) {}
try { in.close(); } catch (IOException ioe) {}
}
}
/**
* Import the private key and certificate chain to a keystore.
* Keystore will be created if it does not exist.
* Private key MUST be first in the stream.
* Closes the stream. 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, non-null.
* @param keyPW the key password, must be at least 6 characters
* @since 0.9.25
*/
public static void storePrivateKey(File ks, String ksPW, String alias, String keyPW,
PrivateKey pk, List<X509Certificate> certs)
throws GeneralSecurityException, IOException {
OutputStream fos = null;
try {
KeyStore keyStore = createKeyStore(ks, ksPW);
keyStore.setKeyEntry(alias, pk, keyPW.toCharArray(), certs.toArray(new Certificate[certs.size()]));
char[] pwchars = ksPW != null ? ksPW.toCharArray() : null;
fos = new SecureFileOutputStream(ks);
keyStore.store(fos, pwchars);
} finally {
if (fos != null) try { fos.close(); } catch (IOException ioe) {}
}
}
/**
* Get a cert out of a keystore
*
* @param ks path to the keystore
* @param ksPW the keystore password, may be null
* @param alias the name of the key
* @return the certificate or null if not found
*/
public static Certificate getCert(File ks, String ksPW, String alias)
throws GeneralSecurityException, IOException {
InputStream fis = null;
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
fis = new FileInputStream(ks);
char[] pwchars = ksPW != null ? ksPW.toCharArray() : null;
keyStore.load(fis, pwchars);
return keyStore.getCertificate(alias);
} finally {
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
}
}
/**
* Pull the cert back OUT of the keystore and save it in Base64-encoded X.509 format
* so the clients can get to it.
*
* @param ks path to the keystore
* @param ksPW the keystore password, may be null
* @param alias the name of the key
* @param certFile output
* @return success
* @since 0.8.3 moved from SSLClientListenerRunner in 0.9.9
*/
public static boolean exportCert(File ks, String ksPW, String alias, File certFile) {
InputStream fis = null;
try {
Certificate cert = getCert(ks, ksPW, alias);
if (cert != null)
return CertUtil.saveCert(cert, certFile);
} catch (GeneralSecurityException gse) {
error("Error saving ASCII SSL keys", gse);
} catch (IOException ioe) {
error("Error saving ASCII SSL keys", ioe);
}
return false;
}
private static void info(String msg) {
log(I2PAppContext.getGlobalContext(), Log.INFO, msg, null);
}
/** @since 0.9.17 */
private static void warn(String msg, Throwable t) {
log(I2PAppContext.getGlobalContext(), Log.WARN, msg, t);
}
private static void error(String msg, Throwable t) {
log(I2PAppContext.getGlobalContext(), Log.ERROR, msg, t);
}
//private static void info(I2PAppContext ctx, String msg) {
// log(ctx, Log.INFO, msg, null);
//}
//private static void error(I2PAppContext ctx, String msg, Throwable t) {
// log(ctx, Log.ERROR, msg, t);
//}
private static void log(I2PAppContext ctx, int level, String msg, Throwable t) {
if (level >= Log.WARN && !ctx.isRouterContext()) {
System.out.println(msg);
if (t != null)
t.printStackTrace();
}
Log l = ctx.logManager().getLog(KeyStoreUtil.class);
l.log(level, msg, t);
}
/**
* Usage: KeyStoreUtil (loads from system keystore)
* KeyStoreUtil foo.ks (loads from system keystore, and from foo.ks keystore if exists, else creates empty)
* KeyStoreUtil certDir (loads from system keystore and all certs in certDir if exists)
* KeyStoreUtil import file.ks file.key alias keypw (imxports private key from file to keystore)
* KeyStoreUtil export file.ks alias keypw (exports private key from keystore)
* KeyStoreUtil keygen file.ks alias keypw (create keypair in keystore)
* KeyStoreUtil keygen2 file.ks alias keypw (create keypair using I2PProvider)
*/
/****
public static void main(String[] args) {
try {
if (args.length > 0 && "import".equals(args[0])) {
testImport(args);
return;
}
if (args.length > 0 && "export".equals(args[0])) {
testExport(args);
return;
}
if (args.length > 0 && "keygen".equals(args[0])) {
testKeygen(args);
return;
}
if (args.length > 0 && "keygen2".equals(args[0])) {
testKeygen2(args);
return;
}
File ksf = (args.length > 0) ? new File(args[0]) : null;
if (ksf != null && !ksf.exists()) {
createKeyStore(ksf, DEFAULT_KEYSTORE_PASSWORD);
System.out.println("Created empty keystore " + ksf);
} else {
KeyStore ks = loadSystemKeyStore();
if (ks != null) {
System.out.println("Loaded system keystore");
int count = countCerts(ks);
System.out.println("Found " + count + " certs");
if (ksf != null && ksf.isDirectory()) {
count = addCerts(ksf, ks);
System.out.println("Found " + count + " certs in " + ksf);
if (count > 0) {
// rerun blacklist as a test
_blacklistLogged = false;
count = removeBlacklistedCerts(ks);
if (count > 0)
System.out.println("Found " + count + " blacklisted certs in " + ksf);
}
}
} else {
System.out.println("FAIL");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void testImport(String[] args) throws Exception {
File ksf = new File(args[1]);
InputStream in = new FileInputStream(args[2]);
String alias = args[3];
String pw = args[4];
importPrivateKey(ksf, DEFAULT_KEYSTORE_PASSWORD, alias, pw, in);
}
private static void testExport(String[] args) throws Exception {
File ksf = new File(args[1]);
String alias = args[2];
String pw = args[3];
exportPrivateKey(ksf, DEFAULT_KEYSTORE_PASSWORD, alias, pw, System.out);
}
private static void testKeygen(String[] args) throws Exception {
File ksf = new File(args[1]);
String alias = args[2];
String pw = args[3];
boolean ok = createKeys(ksf, DEFAULT_KEYSTORE_PASSWORD, alias, "test cname", "test ou",
//DEFAULT_KEY_VALID_DAYS, "EdDSA", 256, pw);
DEFAULT_KEY_VALID_DAYS, "ElGamal", 2048, pw);
System.out.println("genkey ok? " + ok);
}
private static void testKeygen2(String[] args) throws Exception {
// keygen test using the I2PProvider
SigType type = SigType.EdDSA_SHA512_Ed25519;
//SigType type = SigType.ElGamal_SHA256_MODP2048;
java.security.KeyPairGenerator kpg = java.security.KeyPairGenerator.getInstance(type.getBaseAlgorithm().getName());
kpg.initialize(type.getParams());
java.security.KeyPair kp = kpg.generateKeyPair();
java.security.PublicKey jpub = kp.getPublic();
java.security.PrivateKey jpriv = kp.getPrivate();
System.out.println("Encoded private key:");
System.out.println(net.i2p.util.HexDump.dump(jpriv.getEncoded()));
System.out.println("Encoded public key:");
System.out.println(net.i2p.util.HexDump.dump(jpub.getEncoded()));
java.security.Signature jsig = java.security.Signature.getInstance(type.getAlgorithmName());
jsig.initSign(jpriv);
byte[] data = new byte[111];
net.i2p.util.RandomSource.getInstance().nextBytes(data);
jsig.update(data);
byte[] bsig = jsig.sign();
System.out.println("Encoded signature:");
System.out.println(net.i2p.util.HexDump.dump(bsig));
jsig.initVerify(jpub);
jsig.update(data);
boolean ok = jsig.verify(bsig);
System.out.println("verify passed? " + ok);
net.i2p.data.Signature sig = SigUtil.fromJavaSig(bsig, type);
System.out.println("Signature test: " + sig);
}
****/
}