News: Support blocklist in the news feed (proposal 129)

This commit is contained in:
zzz
2016-11-23 13:54:05 +00:00
parent 86c0fe327b
commit 62064da081
6 changed files with 501 additions and 13 deletions

View File

@ -0,0 +1,279 @@
package net.i2p.router.news;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.List;
import java.util.ArrayList;
import net.i2p.I2PAppContext;
import net.i2p.crypto.DirKeyRing;
import net.i2p.crypto.KeyRing;
import net.i2p.crypto.KeyStoreUtil;
import net.i2p.crypto.SigType;
import net.i2p.crypto.SigUtil;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.util.Log;
/**
* One Blocklist.
* Any String fields may be null.
*
* @since 0.9.28
*/
public class BlocklistEntries {
public final List<String> entries, removes;
public String signer;
public String sig;
public String supdated;
public long updated;
private boolean verified;
public static final int MAX_ENTRIES = 2000;
private static final String CONTENT_ROUTER = "router";
public BlocklistEntries(int capacity) {
entries = new ArrayList<String>(capacity);
removes = new ArrayList<String>(4);
}
public synchronized boolean isVerified() {
return verified;
}
public synchronized boolean verify(I2PAppContext ctx) {
if (verified)
return true;
if (signer == null || sig == null || supdated == null)
return false;
Log log = ctx.logManager().getLog(BlocklistEntries.class);
String[] ss = DataHelper.split(sig, ":", 2);
if (ss.length != 2) {
log.error("blocklist feed bad sig: " + sig);
return false;
}
SigType type = SigType.parseSigType(ss[0]);
if (type == null) {
log.error("blocklist feed bad sig: " + sig);
return false;
}
if (!type.isAvailable()) {
log.error("blocklist feed sigtype unavailable: " + sig);
return false;
}
byte[] bsig = Base64.decode(ss[1]);
if (bsig == null) {
log.error("blocklist feed bad sig: " + sig);
return false;
}
Signature ssig;
try {
ssig = new Signature(type, bsig);
} catch (IllegalArgumentException iae) {
log.error("blocklist feed bad sig: " + sig);
return false;
}
// look in both install dir and config dir for the signer cert
KeyRing ring = new DirKeyRing(new File(ctx.getBaseDir(), "certificates"));
PublicKey pubkey;
try {
pubkey = ring.getKey(signer, CONTENT_ROUTER, type);
} catch (IOException ioe) {
log.error("blocklist feed error", ioe);
return false;
} catch (GeneralSecurityException gse) {
log.error("blocklist feed error", gse);
return false;
}
if (pubkey == null) {
boolean diff = true;
try {
diff = !ctx.getBaseDir().getCanonicalPath().equals(ctx.getConfigDir().getCanonicalPath());
} catch (IOException ioe) {}
if (diff) {
ring = new DirKeyRing(new File(ctx.getConfigDir(), "certificates"));
try {
pubkey = ring.getKey(signer, CONTENT_ROUTER, type);
} catch (IOException ioe) {
log.error("blocklist feed error", ioe);
return false;
} catch (GeneralSecurityException gse) {
log.error("blocklist feed error", gse);
return false;
}
}
if (pubkey == null) {
log.error("unknown signer for blocklist feed: " + signer);
return false;
}
}
SigningPublicKey spubkey;
try {
spubkey = SigUtil.fromJavaKey(pubkey, type);
} catch (GeneralSecurityException gse) {
log.error("blocklist feed bad sig: " + sig, gse);
return false;
}
StringBuilder buf = new StringBuilder(256);
buf.append(supdated).append('\n');
for (String s : entries) {
buf.append(s).append('\n');
}
for (String s : removes) {
buf.append('!').append(s).append('\n');
}
byte[] data = DataHelper.getUTF8(buf.toString());
boolean rv = ctx.dsa().verifySignature(ssig, data, spubkey);
if (rv)
log.info("blocklist feed sig ok");
else
log.error("blocklist feed sig verify fail: " + signer);
verified = rv;
return rv;
}
/**
* BlocklistEntries [-p keystorepw] input.txt keystore.ks you@mail.i2p
* File format: One entry per line, # starts a comment, ! starts an unblock entry.
* Single IPv4 or IPv6 address only (no mask allowed), or 44-char base 64 router hash.
* See MAX_ENTRIES above.
*/
public static void main(String[] args) {
if (args.length < 3) {
System.err.println("Usage: BlocklistEntries [-p keystorepw] input.txt keystore.ks you@mail.i2p");
System.exit(1);
}
int st;
String kspass;
if (args[0].equals("-p")) {
kspass = args[1];
st = 2;
} else {
kspass = KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD;
st = 0;
}
String inputFile = args[st++];
String privateKeyFile = args[st++];
String signerName = args[st];
I2PAppContext ctx = new I2PAppContext();
List<String> elist = new ArrayList<String>(16);
List<String> rlist = new ArrayList<String>(4);
StringBuilder buf = new StringBuilder();
long now = System.currentTimeMillis();
String date = RFC3339Date.to3339Date(now);
buf.append(date).append('\n');;
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), "UTF-8"));
String s = null;
while ((s = br.readLine()) != null) {
int index = s.indexOf('#');
if (index == 0)
continue; // comment
if (index > 0)
s = s.substring(0, index);
s = s.trim();
if (s.length() < 7) {
if (s.length() > 0)
System.err.println("Bad line: " + s);
continue;
}
if (s.startsWith("!")) {
rlist.add(s.substring(1));
} else {
elist.add(s);
buf.append(s).append('\n');;
}
}
} catch (IOException ioe) {
System.err.println("load error from " + args[0]);
ioe.printStackTrace();
System.exit(1);
} finally {
if (br != null) try { br.close(); } catch (IOException ioe) {}
}
if (elist.isEmpty() && rlist.isEmpty()) {
System.err.println("nothing to sign");
System.exit(1);
}
if (elist.size() > MAX_ENTRIES) {
System.err.println("too many blocks, max is " + MAX_ENTRIES);
System.exit(1);
}
for (String s : rlist) {
buf.append('!').append(s).append('\n');
}
SigningPrivateKey spk = null;
try {
String keypw = "";
while (keypw.length() < 6) {
System.err.print("Enter password for key \"" + signerName + "\": ");
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");
}
File pkfile = new File(privateKeyFile);
PrivateKey pk = KeyStoreUtil.getPrivateKey(pkfile, kspass, signerName, keypw);
if (pk == null) {
System.out.println("Private key for " + signerName + " not found in keystore " + privateKeyFile);
System.exit(1);
}
spk = SigUtil.fromJavaKey(pk);
} catch (GeneralSecurityException gse) {
System.out.println("Error signing input file '" + inputFile + "'");
gse.printStackTrace();
System.exit(1);
} catch (IOException ioe) {
System.out.println("Error signing input file '" + inputFile + "'");
ioe.printStackTrace();
System.exit(1);
}
SigType type = spk.getType();
byte[] data = DataHelper.getUTF8(buf.toString());
Signature ssig = ctx.dsa().sign(data, spk);
if (ssig == null) {
System.err.println("sign failed");
System.exit(1);
}
String bsig = Base64.encode(ssig.getData());
// verify
BlocklistEntries ble = new BlocklistEntries(elist.size());
ble.entries.addAll(elist);
ble.removes.addAll(rlist);
ble.supdated = date;
ble.signer = signerName;
ble.sig = type.getCode() + ":" + bsig;
boolean ok = ble.verify(ctx);
if (!ok) {
System.err.println("verify failed");
System.exit(1);
}
System.out.println(" <i2p:blocklist updated=\"" + date + "\" signed-by=\"" + signerName + "\" sig=\"" + type.getCode() + ':' + bsig + "\">");
for (String e : elist) {
System.out.println(" <i2p:block>" + e + "</i2p:block>");
}
for (String e : rlist) {
System.out.println(" <i2p:unblock>" + e + "</i2p:unblock>");
}
System.out.println(" </i2p:blocklist>");
}
}

View File

@ -33,6 +33,7 @@ public class NewsXMLParser {
private final Log _log;
private List<NewsEntry> _entries;
private List<CRLEntry> _crlEntries;
private BlocklistEntries _blocklistEntries;
private NewsMetadata _metadata;
private XHTMLMode _mode;
@ -157,12 +158,24 @@ public class NewsXMLParser {
return _crlEntries;
}
/**
* The blocklist entries.
* Must call parse() first.
*
* @return null if none
* @since 0.9.28
*/
public BlocklistEntries getBlocklistEntries() {
return _blocklistEntries;
}
private void extract(Node root) throws I2PParserException {
if (!root.getName().equals("feed"))
throw new I2PParserException("no feed in XML");
_metadata = extractNewsMetadata(root);
_entries = extractNewsEntries(root);
_crlEntries = extractCRLEntries(root);
_blocklistEntries = extractBlocklistEntries(root);
}
private static NewsMetadata extractNewsMetadata(Node feed) throws I2PParserException {
@ -370,7 +383,7 @@ public class NewsXMLParser {
* @return null if none
* @since 0.9.26
*/
private List<CRLEntry> extractCRLEntries(Node feed) throws I2PParserException {
private static List<CRLEntry> extractCRLEntries(Node feed) throws I2PParserException {
Node rev = feed.getNode("i2p:revocations");
if (rev == null)
return null;
@ -397,6 +410,53 @@ public class NewsXMLParser {
return rv;
}
/**
* This does not check for any missing values.
* Any field in a BlocklistEntry may be null.
* Signature is verified here.
*
* @return null if none
* @since 0.9.28
*/
private BlocklistEntries extractBlocklistEntries(Node feed) throws I2PParserException {
Node bl = feed.getNode("i2p:blocklist");
if (bl == null)
return null;
List<Node> entries = getNodes(bl, "i2p:block");
BlocklistEntries rv = new BlocklistEntries(entries.size());
String a = bl.getAttributeValue("signed-by");
if (a.length() > 0)
rv.signer = a;
a = bl.getAttributeValue("sig");
if (a.length() > 0) {
rv.sig = a;
}
a = bl.getAttributeValue("updated");
if (a.length() > 0) {
rv.supdated = a;
long time = RFC3339Date.parse3339Date(a.trim());
if (time > 0)
rv.updated = time;
}
for (Node entry : entries) {
a = entry.getValue();
if (a != null) {
rv.entries.add(a.trim());
}
}
List<Node> rentries = getNodes(bl, "i2p:unblock");
if (entries.isEmpty() && rentries.isEmpty())
return null;
for (Node entry : rentries) {
a = entry.getValue();
if (a != null) {
rv.removes.add(a.trim());
}
}
rv.verify(_context);
return rv;
}
/**
* Helper to get all Nodes matching the name
*

View File

@ -17,6 +17,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -28,8 +29,12 @@ import net.i2p.crypto.SU3File;
import net.i2p.crypto.TrustedUpdate;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.router.Banlist;
import net.i2p.router.Blocklist;
import net.i2p.router.RouterContext;
import net.i2p.router.RouterVersion;
import net.i2p.router.news.BlocklistEntries;
import net.i2p.router.news.CRLEntry;
import net.i2p.router.news.NewsEntry;
import net.i2p.router.news.NewsManager;
@ -41,6 +46,7 @@ import net.i2p.router.web.NewsHelper;
import net.i2p.update.*;
import static net.i2p.update.UpdateType.*;
import static net.i2p.update.UpdateMethod.*;
import net.i2p.util.Addresses;
import net.i2p.util.EepGet;
import net.i2p.util.FileUtil;
import net.i2p.util.Log;
@ -71,6 +77,9 @@ class NewsFetcher extends UpdateRunner {
private boolean _success;
private static final String TEMP_NEWS_FILE = "news.xml.temp";
private static final String PROP_BLOCKLIST_TIME = "router.blocklistVersion";
private static final String BLOCKLIST_DIR = "docs/feed/blocklist";
private static final String BLOCKLIST_FILE = "blocklist.txt";
public NewsFetcher(RouterContext ctx, ConsoleUpdateManager mgr, List<URI> uris) {
super(ctx, mgr, NEWS, uris);
@ -521,6 +530,14 @@ class NewsFetcher extends UpdateRunner {
persistCRLEntries(crlEntries);
else
_log.info("No CRL entries found in news feed");
// Block any new blocklist entries
BlocklistEntries ble = parser.getBlocklistEntries();
if (ble != null && ble.isVerified())
processBlocklistEntries(ble);
else
_log.info("No blocklist entries found in news feed");
// store entries and metadata in old news.xml format
String sudVersion = su3.getVersionString();
String signingKeyName = su3.getSignerString();
@ -607,6 +624,94 @@ class NewsFetcher extends UpdateRunner {
_log.logAlways(Log.WARN, "Stored " + i + " new CRL " + (i > 1 ? "entries" : "entry"));
}
/**
* Process blocklist entries
*
* @since 0.9.28
*/
private void processBlocklistEntries(BlocklistEntries ble) {
long oldTime = _context.getProperty(PROP_BLOCKLIST_TIME, 0L);
if (ble.updated <= oldTime) {
if (_log.shouldWarn())
_log.warn("Not processing blocklist " + new Date(ble.updated) +
", already have " + new Date(oldTime));
return;
}
Blocklist bl = _context.blocklist();
Banlist ban = _context.banlist();
int banned = 0;
for (Iterator<String> iter = ble.entries.iterator(); iter.hasNext(); ) {
String s = iter.next();
if (s.length() == 44) {
byte[] b = Base64.decode(s);
if (b == null || b.length != Hash.HASH_LENGTH) {
iter.remove();
continue;
}
Hash h = Hash.create(b);
if (!ban.isBanlistedForever(h))
ban.banlistRouterForever(h, "News feed");
} else {
byte[] ip = Addresses.getIP(s);
if (ip == null) {
iter.remove();
continue;
}
if (!bl.isBlocklisted(ip))
bl.add(ip);
}
if (++banned >= BlocklistEntries.MAX_ENTRIES) {
// prevent somebody from destroying the whole network
break;
}
}
for (String s : ble.removes) {
if (s.length() == 44) {
byte[] b = Base64.decode(s);
if (b == null || b.length != Hash.HASH_LENGTH)
continue;
Hash h = Hash.create(b);
if (ban.isBanlistedForever(h))
ban.unbanlistRouter(h);
} else {
byte[] ip = Addresses.getIP(s);
if (ip == null)
continue;
if (bl.isBlocklisted(ip))
bl.remove(ip);
}
}
// Save the blocks. We do not save the unblocks.
File f = new SecureFile(_context.getConfigDir(), BLOCKLIST_DIR);
f.mkdirs();
f = new File(f, BLOCKLIST_FILE);
boolean fail = false;
BufferedWriter out = null;
try {
out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(f), "UTF-8"));
out.write("# ");
out.write(ble.supdated);
out.newLine();
for (String s : ble.entries) {
s = s.replace(':', ';'); // IPv6
out.write("Blocklist Feed:");
out.write(s);
out.newLine();
}
} catch (IOException ioe) {
_log.error("Error writing blocklist", ioe);
fail = true;
} finally {
if (out != null) try {
out.close();
} catch (IOException ioe) {}
}
if (!fail)
_context.router().saveConfig(PROP_BLOCKLIST_TIME, Long.toString(ble.updated));
if (_log.shouldWarn())
_log.warn("Processed " + ble.entries.size() + " blocks and " + ble.removes.size() + " unblocks from news feed");
}
/**
* Output in the old format.
*
@ -617,7 +722,7 @@ class NewsFetcher extends UpdateRunner {
NewsMetadata.Release latestRelease = data.releases.get(0);
Writer out = null;
try {
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(to), "UTF-8"));
out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(to), "UTF-8"));
out.write("<!--\n");
// update metadata in old format
out.write("<i2p.release ");