Crypto: Add support for more alt names in certs (tickets #2159, #2160)

Set alt names for console cert
Use utils to validate console IP addresses
This commit is contained in:
zzz
2018-02-25 14:17:01 +00:00
parent 622c6801ae
commit 79baf70f9a
3 changed files with 242 additions and 58 deletions

View File

@ -42,6 +42,8 @@ import net.i2p.util.SecureDirectory;
import net.i2p.util.I2PSSLSocketFactory;
import net.i2p.util.SystemVersion;
import org.apache.http.conn.util.InetAddressUtils;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
@ -516,17 +518,15 @@ public class RouterConsoleRunner implements RouterApp {
try {
// Test before we add the connector, because Jetty 6 won't start if any of the
// connectors are bad
InetAddress test = InetAddress.getByName(host);
if ((!hasIPV6) && (!(test instanceof Inet4Address)))
if ((!hasIPV6) && InetAddressUtils.isIPv6Address(host))
throw new IOException("IPv6 addresses unsupported");
if ((!hasIPV4) && (test instanceof Inet4Address))
if ((!hasIPV4) && InetAddressUtils.isIPv4Address(host))
throw new IOException("IPv4 addresses unsupported");
ServerSocket testSock = null;
try {
// On Windows, this was passing and Jetty was still failing,
// possibly due to %scope_id ???
// https://issues.apache.org/jira/browse/ZOOKEEPER-667
//testSock = new ServerSocket(0, 0, test);
// so do exactly what Jetty does in SelectChannelConnector.open()
testSock = new ServerSocket();
InetSocketAddress isa = new InetSocketAddress(host, 0);
@ -574,7 +574,23 @@ public class RouterConsoleRunner implements RouterApp {
}
if (sslPort > 0) {
File keyStore = new File(_context.getConfigDir(), "keystore/console.ks");
if (verifyKeyStore(keyStore)) {
// Put the list of hosts together early, so we can put it in the selfsigned cert.
StringTokenizer tok = new StringTokenizer(_sslListenHost, " ,");
Set<String> altNames = new HashSet<String>(4);
while (tok.hasMoreTokens()) {
String s = tok.nextToken().trim();
if (!s.equals("0.0.0.0") && !s.equals("::") &&
!s.equals("0:0:0:0:0:0:0:0"))
altNames.add(s);
}
String allowed = _context.getProperty(PROP_ALLOWED_HOSTS);
if (allowed != null) {
tok = new StringTokenizer(allowed, " ,");
while (tok.hasMoreTokens()) {
altNames.add(tok.nextToken().trim());
}
}
if (verifyKeyStore(keyStore, altNames)) {
// the keystore path and password
SslContextFactory sslFactory = new SslContextFactory(keyStore.getAbsolutePath());
sslFactory.setKeyStorePassword(_context.getProperty(PROP_KEYSTORE_PASSWORD, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD));
@ -585,22 +601,20 @@ public class RouterConsoleRunner implements RouterApp {
sslFactory.addExcludeCipherSuites(I2PSSLSocketFactory.EXCLUDE_CIPHERS.toArray(
new String[I2PSSLSocketFactory.EXCLUDE_CIPHERS.size()]));
List<String> hosts = new ArrayList<String>(2);
StringTokenizer tok = new StringTokenizer(_sslListenHost, " ,");
tok = new StringTokenizer(_sslListenHost, " ,");
while (tok.hasMoreTokens()) {
String host = tok.nextToken().trim();
// doing it this way means we don't have to escape an IPv6 host with []
try {
// Test before we add the connector, because Jetty 6 won't start if any of the
// connectors are bad
InetAddress test = InetAddress.getByName(host);
if ((!hasIPV6) && (!(test instanceof Inet4Address)))
if ((!hasIPV6) && InetAddressUtils.isIPv6Address(host))
throw new IOException("IPv6 addresses unsupported");
if ((!hasIPV4) && (test instanceof Inet4Address))
if ((!hasIPV4) && InetAddressUtils.isIPv4Address(host))
throw new IOException("IPv4 addresses unsupported");
ServerSocket testSock = null;
try {
// see comments above
//testSock = new ServerSocket(0, 0, test);
testSock = new ServerSocket();
InetSocketAddress isa = new InetSocketAddress(host, 0);
testSock.bind(isa);
@ -839,30 +853,28 @@ public class RouterConsoleRunner implements RouterApp {
* @return success if it exists and we have a password, or it was created successfully.
* @since 0.8.3
*/
private boolean verifyKeyStore(File ks) {
private boolean verifyKeyStore(File ks, Set<String> altNames) {
if (ks.exists()) {
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());
return rv;
}
return createKeyStore(ks);
return createKeyStore(ks, altNames);
}
/**
* Call out to keytool to create a new keystore with a keypair in it.
* Trying to do this programatically is a nightmare, requiring either BouncyCastle
* libs or using proprietary Sun libs, and it's a huge mess.
* Create a new keystore with a keypair in it.
*
* @return success
* @since 0.8.3
*/
private boolean createKeyStore(File ks) {
private boolean createKeyStore(File ks, Set<String> altNames) {
// make a random 48 character password (30 * 8 / 5)
String keyPassword = KeyStoreUtil.randomString();
String cname = "localhost";
boolean success = KeyStoreUtil.createKeys(ks, "console", cname, "Console", keyPassword);
boolean success = KeyStoreUtil.createKeys(ks, "console", cname, altNames, "Console", keyPassword);
if (success) {
success = ks.exists();
if (success) {
@ -883,7 +895,8 @@ public class RouterConsoleRunner implements RouterApp {
}
if (success) {
System.err.println("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" +
"The certificate was generated randomly, and is not associated with your " +
"The certificate was generated randomly.\n" +
"Unless you have changed the default settings, the certificate is not associated with your " +
"IP address, host name, router identity, or destination keys.");
} else {
System.err.println("Failed to create console SSL keystore.\n" +

View File

@ -467,7 +467,30 @@ public final class KeyStoreUtil {
*/
public static boolean createKeys(File ks, String alias, String cname, String ou,
String keyPW) {
return createKeys(ks, DEFAULT_KEYSTORE_PASSWORD, alias, cname, ou,
return createKeys(ks, DEFAULT_KEYSTORE_PASSWORD, alias, cname, null, 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.
* 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. localhost. 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 e.g. console
* @param keyPW the key password, must be at least 6 characters
*
* @return success
* @since 0.9.34 added altNames param
*/
public static boolean createKeys(File ks, String alias, String cname, Set<String> altNames, String ou,
String keyPW) {
return createKeys(ks, DEFAULT_KEYSTORE_PASSWORD, alias, cname, altNames, ou,
DEFAULT_KEY_VALID_DAYS, DEFAULT_KEY_ALGORITHM, DEFAULT_KEY_SIZE, keyPW);
}
@ -494,12 +517,42 @@ public final class KeyStoreUtil {
*/
public static boolean createKeys(File ks, String ksPW, String alias, String cname, String ou,
int validDays, String keyAlg, int keySize, String keyPW) {
return createKeys(ks, ksPW, alias, cname, null, ou, validDays, keyAlg, keySize, 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. localhost. 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 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.9.34 added altNames param
*/
public static boolean createKeys(File ks, String ksPW, String alias, String cname, Set<String> altNames, String ou,
int validDays, String keyAlg, int keySize, String keyPW) {
boolean useKeytool = I2PAppContext.getGlobalContext().getBooleanProperty("crypto.useExternalKeytool");
if (useKeytool) {
if (altNames != null)
throw new IllegalArgumentException("can't do SAN in keytool");
return createKeysCLI(ks, ksPW, alias, cname, ou, validDays, keyAlg, keySize, keyPW);
} else {
try {
createKeysAndCRL(ks, ksPW, alias, cname, ou, validDays, keyAlg, keySize, keyPW);
createKeysAndCRL(ks, ksPW, alias, cname, altNames, ou, validDays, keyAlg, keySize, keyPW);
return true;
} catch (GeneralSecurityException gse) {
error("Create keys error", gse);
@ -546,6 +599,46 @@ public final class KeyStoreUtil {
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 {
return createKeysAndCRL(ks, ksPW, alias, cname, null, ou, validDays, keyAlg, keySize, 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. localhost. 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 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.34 added altNames param
*/
public static Object[] createKeysAndCRL(File ks, String ksPW, String alias, String cname, Set<String> altNames, 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)) {
@ -556,7 +649,7 @@ public final class KeyStoreUtil {
}
if (type == null)
throw new GeneralSecurityException("Unsupported algorithm/size: " + keyAlg + '/' + keySize);
return createKeysAndCRL(ks, ksPW, alias, cname, ou, validDays, type, keyPW);
return createKeysAndCRL(ks, ksPW, alias, cname, altNames, ou, validDays, type, keyPW);
}
/**
@ -592,13 +685,51 @@ public final class KeyStoreUtil {
public static Object[] createKeysAndCRL(File ks, String ksPW, String alias, String cname, String ou,
int validDays, SigType type, String keyPW)
throws GeneralSecurityException, IOException {
return createKeysAndCRL(ks, ksPW, alias, cname, null, 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. localhost. 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 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.34 added altNames param
*/
public static Object[] createKeysAndCRL(File ks, String ksPW, String alias, String cname, Set<String> altNames, 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, "I2P", "I2P Anonymous Network", null, null, validDays, type);
Object[] rv = SelfSignedGenerator.generate(cname, altNames, ou, "I2P", "I2P Anonymous Network", null, null, validDays, type);
//PublicKey jpub = (PublicKey) rv[0];
PrivateKey jpriv = (PrivateKey) rv[1];
X509Certificate cert = (X509Certificate) rv[2];

View File

@ -18,8 +18,10 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import javax.crypto.interfaces.DHPublicKey;
@ -27,12 +29,15 @@ import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.security.auth.x500.X500Principal;
import org.apache.http.conn.util.InetAddressUtils;
import static net.i2p.crypto.SigUtil.intToASN1;
import net.i2p.data.DataHelper;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.SimpleDataStructure;
import net.i2p.util.Addresses;
import net.i2p.util.HexDump;
import net.i2p.util.RandomSource;
import net.i2p.util.SecureFileOutputStream;
@ -42,10 +47,12 @@ import net.i2p.util.SystemVersion;
* Generate keys and a selfsigned certificate, suitable for
* storing in a Keystore with KeyStoreUtil.storePrivateKey().
* All done programatically, no keytool, no BC libs, no sun classes.
* Ref: RFC 2459
* Ref: RFC 2459, RFC 5280
*
* This is coded to create a cert that is similar to what comes out of keytool,
* even if I don't understand all of it.
* This is coded to create a cert that is similar to what comes out of keytool.
*
* NOTE: Recommended use is via KeyStoreUtil.createKeys() and related methods.
* This API may not be stable.
*
* @since 0.9.25
*/
@ -108,6 +115,29 @@ public final class SelfSignedGenerator {
*/
public static Object[] generate(String cname, String ou, String o, String l, String st, String c,
int validDays, SigType type) throws GeneralSecurityException {
return generate(cname, null, ou, o, l, st, c, validDays, type);
}
/**
* @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
*/
public static Object[] generate(String cname, Set<String> altNames, String ou, String o, String l, String st, String c,
int validDays, SigType type) throws GeneralSecurityException {
SimpleDataStructure[] keys = KeyGenerator.getInstance().generateSigningKeys(type);
SigningPublicKey pub = (SigningPublicKey) keys[0];
SigningPrivateKey priv = (SigningPrivateKey) keys[1];
@ -132,7 +162,7 @@ public final class SelfSignedGenerator {
}
byte[] sigoid = getEncodedOIDSeq(oid);
byte[] tbs = genTBS(cname, ou, o, l, st, c, validDays, sigoid, jpub);
byte[] tbs = genTBS(cname, altNames, ou, o, l, st, c, validDays, sigoid, jpub);
int tbslen = tbs.length;
Signature sig = DSAEngine.getInstance().sign(tbs, priv);
@ -261,13 +291,15 @@ public final class SelfSignedGenerator {
/**
* @param cname the common name, non-null
* @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
*/
private static byte[] genTBS(String cname, String ou, String o, String l, String st, String c,
private static byte[] genTBS(String cname, Set<String> altNames, String ou, String o, String l, String st, String c,
int validDays, byte[] sigoid, PublicKey jpub) throws GeneralSecurityException {
// a0 ???, int = 2
byte[] version = { (byte) 0xa0, 3, 2, 1, 2 };
@ -298,7 +330,7 @@ public final class SelfSignedGenerator {
byte[] subject = issuer;
byte[] pubbytes = jpub.getEncoded();
byte[] extbytes = getExtensions(pubbytes, cname);
byte[] extbytes = getExtensions(pubbytes, cname, altNames);
int len = version.length + serial.length + sigoid.length + issuer.length +
validity.length + subject.length + pubbytes.length + extbytes.length;
@ -494,9 +526,11 @@ public final class SelfSignedGenerator {
* Ref: RFC 5280
*
* @param pubbytes bit string
* @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.
* @return ASN.1 encoded object
*/
private static byte[] getExtensions(byte[] pubbytes, String cname) {
private static byte[] getExtensions(byte[] pubbytes, String cname, Set<String> altNames) {
// RFC 2549 sec. 4.2.1.2
// subject public key identifier is the sha1 hash of the bit string of the public key
// without the tag, length, and igore fields
@ -530,21 +564,27 @@ public final class SelfSignedGenerator {
int wrap3len = spaceFor(TRUE.length);
int ext3len = oid3.length + TRUE.length + spaceFor(wrap3len);
// TODO if IP address, encode as 4 or 16 bytes
byte[] cnameBytes = DataHelper.getASCII(cname);
int wrap41len = spaceFor(cnameBytes.length);
// only used for CA
byte[] ipv4;
byte[] ipv6;
int wrap41len = 0;
if (altNames == null)
altNames = new HashSet<String>(4);
else
altNames.remove("0:0:0:0:0:0:0:1"); // We don't want dup of "::1"
altNames.add(cname);
final boolean isCA = !cname.contains("@");
if (isCA) {
ipv4 = new byte[] { 127, 0, 0, 1 };
ipv6 = new byte[16];
ipv6[15] = 1;
wrap41len += spaceFor(ipv4.length) + spaceFor(ipv6.length);
} else {
ipv4 = null;
ipv6 = null;
altNames.add("localhost");
altNames.add("127.0.0.1");
altNames.add("::1");
}
for (String n : altNames) {
int len;
if (InetAddressUtils.isIPv4Address(n))
len = 4;
else if (InetAddressUtils.isIPv6Address(n))
len = 16;
else
len = n.length();
wrap41len += spaceFor(len);
}
int wrap4len = spaceFor(wrap41len);
int ext4len = oid4.length + spaceFor(wrap4len);
@ -643,26 +683,26 @@ public final class SelfSignedGenerator {
idx = intToASN1(rv, idx, ext4len);
System.arraycopy(oid4, 0, rv, idx, oid4.length);
idx += oid4.length;
// octet string wraps a sequence containing a choice 2 (DNSName) IA5String
// followed by two byteArrays (IP addresses)
// octet string wraps a sequence containing the names and IP addresses
rv[idx++] = (byte) 0x04;
idx = intToASN1(rv, idx, wrap4len);
rv[idx++] = (byte) 0x30;
idx = intToASN1(rv, idx, wrap41len);
// TODO if IP address, encode as 0x87
rv[idx++] = (byte) (isCA ? 0x82 : 0x81); // choice, dNSName or rfc822Name, IA5String implied
idx = intToASN1(rv, idx, cnameBytes.length);
System.arraycopy(cnameBytes, 0, rv, idx, cnameBytes.length);
idx += cnameBytes.length;
if (isCA) {
rv[idx++] = (byte) 0x87; // choice, octet string for IP address
idx = intToASN1(rv, idx, ipv4.length);
System.arraycopy(ipv4, 0, rv, idx, ipv4.length);
idx += ipv4.length;
rv[idx++] = (byte) 0x87; // choice, octet string for IP address
idx = intToASN1(rv, idx, ipv6.length);
System.arraycopy(ipv6, 0, rv, idx, ipv6.length);
idx += ipv6.length;
for (String n : altNames) {
byte[] b;
if (InetAddressUtils.isIPv4Address(n) ||
InetAddressUtils.isIPv6Address(n)) {
b = Addresses.getIP(n);
if (b == null) // shouldn't happen
throw new IllegalArgumentException("fail " + n);
rv[idx++] = (byte) 0x87; // choice, octet string for IP address
} else {
b = DataHelper.getASCII(n);
rv[idx++] = (byte) (isCA ? 0x82 : 0x81); // choice, dNSName or rfc822Name, IA5String implied
}
idx = intToASN1(rv, idx, b.length);
System.arraycopy(b, 0, rv, idx, b.length);
idx += b.length;
}
// Policy