package net.i2p.router.crypto;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.security.cert.X509CRL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.crypto.CertUtil;
import net.i2p.crypto.KeyStoreUtil;
import net.i2p.crypto.SigType;
import net.i2p.crypto.SigUtil;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.RouterContext;
import net.i2p.router.StatisticsManager;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
import net.i2p.util.SecureDirectory;
/**
* Utilities for creating, storing, retrieving the signing keys for
* the netdb family feature
*
* @since 0.9.24
*/
public class FamilyKeyCrypto {
private final RouterContext _context;
private final Log _log;
private final Map<Hash, String> _verified;
private final Set<Hash> _negativeCache;
private final Set<Hash> _ourFamily;
// following for verification only, otherwise null
private final String _fname;
private final SigningPrivateKey _privkey;
private final SigningPublicKey _pubkey;
public static final String PROP_KEYSTORE_PASSWORD = "netdb.family.keystorePassword";
public static final String PROP_FAMILY_NAME = "netdb.family.name";
public static final String PROP_KEY_PASSWORD = "netdb.family.keyPassword";
public static final String CERT_SUFFIX = ".crt";
public static final String CRL_SUFFIX = ".crl";
public static final String KEYSTORE_PREFIX = "family-";
public static final String KEYSTORE_SUFFIX = ".ks";
public static final String CN_SUFFIX = ".family.i2p.net";
private static final int DEFAULT_KEY_VALID_DAYS = 3652; // 10 years
// Note that we can't use RSA here, as the b64 sig would exceed the 255 char limit for a Mapping
// Note that we can't use EdDSA here, as keystore doesn't know how, and encoding/decoding is unimplemented
private static final String DEFAULT_KEY_ALGORITHM = SigType.ECDSA_SHA256_P256.isAvailable() ? "EC" : "DSA";
private static final int DEFAULT_KEY_SIZE = SigType.ECDSA_SHA256_P256.isAvailable() ? 256 : 1024;
//private static final String DEFAULT_KEY_ALGORITHM = "EdDSA";
//private static final int DEFAULT_KEY_SIZE = 256;
private static final String KS_DIR = "keystore";
private static final String CERT_DIR = "certificates/family";
private static final String CRL_DIR = "crls";
public static final String OPT_NAME = "family";
public static final String OPT_SIG = "family.sig";
public static final String OPT_KEY = "family.key";
/**
* For signing and verification.
*
* If the context property netdb.family.name is set, this can be used for signing,
* else only for verification.
*/
public FamilyKeyCrypto(RouterContext context) throws GeneralSecurityException {
_context = context;
_log = _context.logManager().getLog(FamilyKeyCrypto.class);
_fname = _context.getProperty(PROP_FAMILY_NAME);
if (_fname != null) {
if (_fname.contains("/") || _fname.contains("\\") ||
_fname.contains("..") || (new File(_fname)).isAbsolute() ||
_fname.length() <= 0)
throw new GeneralSecurityException("Illegal family name: " + _fname);
}
_privkey = (_fname != null) ? initialize() : null;
_pubkey = (_privkey != null) ? _privkey.toPublic() : null;
_verified = new ConcurrentHashMap<Hash, String>(4);
_negativeCache = new ConcurrentHashSet<Hash>(4);
_ourFamily = (_privkey != null) ? new ConcurrentHashSet<Hash>(4) : Collections.<Hash>emptySet();
}
/**
* Create (if necessary) and load the key store, then run.
*/
private SigningPrivateKey initialize() throws GeneralSecurityException {
File dir = new SecureDirectory(_context.getConfigDir(), KS_DIR);
File keyStore = new File(dir, KEYSTORE_PREFIX + _fname + KEYSTORE_SUFFIX);
verifyKeyStore(keyStore);
return getPrivKey(keyStore);
}
/**
* Clears the caches
*/
public void shutdown() {
_verified.clear();
_negativeCache.clear();
}
/**
* Caller must add family to RI also.
* throws on all errors
*
* @param family non-null, must match that we were initialized with or will throw GSE
* @param h non-null
* @return non-null options to be added to the RI
* @throws GeneralSecurityException on null hash, null or changed family, or signing error
*/
public Map<String, String> sign(String family, Hash h) throws GeneralSecurityException {
if (_privkey == null) {
_log.logAlways(Log.WARN, "family name now set, must restart router to generate or load keys");
throw new GeneralSecurityException("family name now set, must restart router to generate or load keys");
}
if (h == null)
throw new GeneralSecurityException("null router hash");
if (!_fname.equals(family)) {
_log.logAlways(Log.WARN, "family name changed, must restart router to generate or load new keys");
throw new GeneralSecurityException("family name changed, must restart router to generate or load new keys");
}
byte[] nb = DataHelper.getUTF8(_fname);
int len = nb.length + Hash.HASH_LENGTH;
byte[] b = new byte[len];
System.arraycopy(nb, 0, b, 0, nb.length);
System.arraycopy(h.getData(), 0, b, nb.length, Hash.HASH_LENGTH);
Signature sig = _context.dsa().sign(b, _privkey);
if (sig == null)
throw new GeneralSecurityException("sig failed");
Map<String, String> rv = new HashMap<String, String>(3);
rv.put(OPT_NAME, family);
rv.put(OPT_KEY, _pubkey.getType().getCode() + ":" + _pubkey.toBase64());
rv.put(OPT_SIG, sig.toBase64());
return rv;
}
/**
* Do we have a valid family?
* @since 0.9.28
*/
public boolean hasFamily() {
return _pubkey != null;
}
/**
* Get verified members of our family.
* Will not contain ourselves.
*
* @return non-null, not a copy, do not modify
* @since 0.9.28
*/
public Set<Hash> getOurFamily() {
return _ourFamily;
}
/**
* Get our family name.
*
* @return name or null
* @since 0.9.28
*/
public String getOurFamilyName() {
return _fname;
}
/**
* Verify the family signature in a RouterInfo.
* @return true if good sig or if no family specified at all
*/
public boolean verify(RouterInfo ri) {
String name = ri.getOption(OPT_NAME);
if (name == null)
return true;
return verify(ri, name);
}
/**
* Verify the family in a RouterInfo matches ours and the signature is good.
* Returns false if we don't have a family and sig, or they don't.
* Returns false for ourselves.
*
* @return true if family matches with good sig
* @since 0.9.28
*/
public boolean verifyOurFamily(RouterInfo ri) {
if (_pubkey == null)
return false;
String name = ri.getOption(OPT_NAME);
if (!_fname.equals(name))
return false;
Hash h = ri.getHash();
if (_ourFamily.contains(h))
return true;
if (h.equals(_context.routerHash()))
return false;
boolean rv = verify(ri, name);
if (rv) {
_ourFamily.add(h);
_log.logAlways(Log.INFO, "Found and verified member of our family (" + _fname + "): " + h);
} else {
if (_log.shouldWarn())
_log.warn("Found spoofed member of our family (" + _fname + "): " + h);
}
return rv;
}
/**
* Verify the family in a RouterInfo, name already retrieved
* @since 0.9.28
*/
private boolean verify(RouterInfo ri, String name) {
Hash h = ri.getHash();
String ssig = ri.getOption(OPT_SIG);
if (ssig == null) {
if (_log.shouldInfo())
_log.info("No sig for " + h + ' ' + name);
return false;
}
String nameAndSig = _verified.get(h);
String riNameAndSig = name + ssig;
if (nameAndSig != null) {
if (nameAndSig.equals(riNameAndSig))
return true;
// name or sig changed
_verified.remove(h);
}
SigningPublicKey spk;
if (name.equals(_fname)) {
// us
spk = _pubkey;
} else {
if (_negativeCache.contains(h))
return false;
spk = loadCert(name);
if (spk == null) {
// look for a b64 key in the RI
String skey = ri.getOption(OPT_KEY);
if (skey != null) {
int colon = skey.indexOf(':');
// switched from ';' to ':' during dev, remove this later
if (colon < 0)
colon = skey.indexOf(';');
if (colon > 0) {
try {
int code = Integer.parseInt(skey.substring(0, colon));
SigType type = SigType.getByCode(code);
if (type != null) {
byte[] bkey = Base64.decode(skey.substring(colon + 1));
if (bkey != null) {
spk = new SigningPublicKey(type, bkey);
}
}
} catch (NumberFormatException e) {
if (_log.shouldInfo())
_log.info("Bad b64 family key: " + ri, e);
} catch (IllegalArgumentException e) {
if (_log.shouldInfo())
_log.info("Bad b64 family key: " + ri, e);
} catch (ArrayIndexOutOfBoundsException e) {
if (_log.shouldInfo())
_log.info("Bad b64 family key: " + ri, e);
}
}
}
if (spk == null) {
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("No cert or valid key for " + h + ' ' + name);
return false;
}
}
}
if (!spk.getType().isAvailable()) {
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("Unsupported crypto for sig for " + h);
return false;
}
byte[] bsig = Base64.decode(ssig);
if (bsig == null) {
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("Bad sig for " + h + ' ' + name + ' ' + ssig);
return false;
}
Signature sig;
try {
sig = new Signature(spk.getType(), bsig);
} catch (IllegalArgumentException iae) {
// wrong size (type mismatch)
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("Bad sig for " + ri, iae);
return false;
}
byte[] nb = DataHelper.getUTF8(name);
byte[] b = new byte[nb.length + Hash.HASH_LENGTH];
System.arraycopy(nb, 0, b, 0, nb.length);
System.arraycopy(ri.getHash().getData(), 0, b, nb.length, Hash.HASH_LENGTH);
boolean rv = _context.dsa().verifySignature(sig, b, spk);
if (rv)
_verified.put(h, riNameAndSig);
else
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("Verified? " + rv + " for " + h + ' ' + name + ' ' + ssig);
return rv;
}
/**
* @return success if it exists and we have a password, or it was created successfully.
* @throws GeneralSecurityException on keystore error
*/
private void verifyKeyStore(File ks) throws GeneralSecurityException {
if (ks.exists()) {
if (_context.getProperty(PROP_KEY_PASSWORD) == null) {
String s ="Family key error, must set " + PROP_KEY_PASSWORD + " in " +
(new File(_context.getConfigDir(), "router.config")).getAbsolutePath();
_log.error(s);
throw new GeneralSecurityException(s);
}
return;
}
File dir = ks.getParentFile();
if (!dir.exists()) {
File sdir = new SecureDirectory(dir.getAbsolutePath());
if (!sdir.mkdirs()) {
String s ="Family key error, must set " + PROP_KEY_PASSWORD + " in " +
(new File(_context.getConfigDir(), "router.config")).getAbsolutePath();
_log.error(s);
throw new GeneralSecurityException(s);
}
}
try {
createKeyStore(ks);
} catch (IOException ioe) {
throw new GeneralSecurityException("Failed to create NetDb family keystore", ioe);
}
}
/**
* 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.
* If successful, stores the keystore password and key password in router.config.
*
* @throws GeneralSecurityException on all errors
*/
private void createKeyStore(File ks) throws GeneralSecurityException, IOException {
// make a random 48 character password (30 * 8 / 5)
String keyPassword = KeyStoreUtil.randomString();
// and one for the cname
String cname = _fname + CN_SUFFIX;
Object[] rv = KeyStoreUtil.createKeysAndCRL(ks, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD, _fname, cname, "family",
DEFAULT_KEY_VALID_DAYS, DEFAULT_KEY_ALGORITHM,
DEFAULT_KEY_SIZE, keyPassword);
Map<String, String> changes = new HashMap<String, String>();
changes.put(PROP_KEYSTORE_PASSWORD, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD);
changes.put(PROP_KEY_PASSWORD, keyPassword);
changes.put(PROP_FAMILY_NAME, _fname);
_context.router().saveConfig(changes, null);
_log.logAlways(Log.INFO, "Created new private key for netdb family \"" + _fname +
"\" in keystore: " + ks.getAbsolutePath() + "\n" +
"Copy the keystore to the other routers in the family,\n" +
"and add the following entries to their router.config file:\n" +
PROP_FAMILY_NAME + '=' + _fname + '\n' +
PROP_KEYSTORE_PASSWORD + '=' + KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD + '\n' +
PROP_KEY_PASSWORD + '=' + keyPassword);
X509Certificate cert = (X509Certificate) rv[2];
exportCert(cert);
X509CRL crl = (X509CRL) rv[3];
exportCRL(ks.getParentFile(), crl);
}
/**
* Save the public key certificate
* so the clients can get to it.
*/
private void exportCert(X509Certificate cert) {
File sdir = new SecureDirectory(_context.getConfigDir(), CERT_DIR);
if (sdir.exists() || sdir.mkdirs()) {
String name = _fname.replace("@", "_at_") + CERT_SUFFIX;
File out = new File(sdir, name);
boolean success = CertUtil.saveCert(cert, out);
if (success) {
_log.logAlways(Log.INFO, "Created new public key certificate for netdb family \"" + _fname +
"\" in file: " + out.getAbsolutePath() + "\n" +
"The certificate will be associated with your router identity.\n" +
"Copy the certificate to the directory $I2P/" + CERT_DIR + " for each of the other routers in the family.\n" +
"Give this certificate to an I2P developer for inclusion in the next I2P release.");
} else {
_log.error("Error saving family key certificate");
}
} else {
_log.error("Error saving family key certificate");
}
}
/**
* Save the CRL just in case.
* @param ksdir parent of directory to save in
* @since 0.9.25
*/
private void exportCRL(File ksdir, X509CRL crl) {
File sdir = new SecureDirectory(ksdir, CRL_DIR);
if (sdir.exists() || sdir.mkdirs()) {
String name = KEYSTORE_PREFIX + _fname.replace("@", "_at_") + '-' + System.currentTimeMillis() + CRL_SUFFIX;
File out = new File(sdir, name);
boolean success = CertUtil.saveCRL(crl, out);
if (success) {
_log.logAlways(Log.INFO, "Created certificate revocation list (CRL) for netdb family \"" + _fname +
"\" in file: " + out.getAbsolutePath() + "\n" +
"Back up the keystore and CRL files and keep them secure.\n" +
"If your private key is ever compromised, give the CRL to an I2P developer for publication.");
} else {
_log.error("Error saving family key CRL");
}
} else {
_log.error("Error saving family key CRL");
}
}
/**
* Load a public key from a cert.
*
* @return null on all errors
*/
private SigningPublicKey loadCert(String familyName) {
if (familyName.contains("/") || familyName.contains("\\") ||
familyName.contains("..") || (new File(familyName)).isAbsolute())
return null;
familyName = familyName.replace("@", "_at_");
File dir = new File(_context.getBaseDir(), CERT_DIR);
File file = new File(dir, familyName + CERT_SUFFIX);
if (!file.exists())
return null;
try {
PublicKey pk = CertUtil.loadKey(file);
return SigUtil.fromJavaKey(pk);
} catch (GeneralSecurityException gse) {
_log.error("Error loading family key " + familyName, gse);
} catch (IOException ioe) {
_log.error("Error loading family key " + familyName, ioe);
}
return null;
}
/**
* Get the private key from the keystore
* @return non-null, throws on all errors
*/
private SigningPrivateKey getPrivKey(File ks) throws GeneralSecurityException {
String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD);
String keyPass = _context.getProperty(PROP_KEY_PASSWORD);
if (keyPass == null)
throw new GeneralSecurityException("No key password, set " + PROP_KEY_PASSWORD +
" in " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath());
try {
PrivateKey pk = KeyStoreUtil.getPrivateKey(ks, ksPass, _fname, keyPass);
if (pk == null)
throw new GeneralSecurityException("Family key not found: " + _fname);
return SigUtil.fromJavaKey(pk);
} catch (IOException ioe) {
throw new GeneralSecurityException("Error loading family key " + _fname, ioe);
}
}
}