diff --git a/apps/addressbook/java/src/addressbook/AddressBook.java b/apps/addressbook/java/src/addressbook/AddressBook.java index 547cb71cb5..ca1e1916e0 100644 --- a/apps/addressbook/java/src/addressbook/AddressBook.java +++ b/apps/addressbook/java/src/addressbook/AddressBook.java @@ -159,6 +159,9 @@ public class AddressBook { return this.addresses.toString(); } + private static final int MIN_DEST_LENGTH = 516; + private static final int MAX_DEST_LENGTH = MIN_DEST_LENGTH + 100; // longer than any known cert type for now + /** * Do basic validation of the hostname and dest * hostname was already converted to lower case by ConfigParser.parse() @@ -184,8 +187,8 @@ public class AddressBook { (! host.endsWith(".router.i2p")) && (! host.endsWith(".console.i2p")) && - dest.length() == 516 && - dest.endsWith("AAAA") && + ((dest.length() == MIN_DEST_LENGTH && dest.endsWith("AAAA")) || + (dest.length() > MIN_DEST_LENGTH && dest.length() <= MAX_DEST_LENGTH)) && dest.replaceAll("[a-zA-Z0-9~-]", "").length() == 0 ; } diff --git a/apps/routerconsole/jsp/default.css b/apps/routerconsole/jsp/default.css index d308818c10..b5a63a1ad4 100644 --- a/apps/routerconsole/jsp/default.css +++ b/apps/routerconsole/jsp/default.css @@ -45,6 +45,7 @@ div.routersummary { color: inherit; font-size: small; clear: left; /* fixes a bug in Opera */ + overflow: auto; } div.warning { diff --git a/core/java/src/com/nettgryppa/security/HashCash.java b/core/java/src/com/nettgryppa/security/HashCash.java new file mode 100644 index 0000000000..7d4e16e927 --- /dev/null +++ b/core/java/src/com/nettgryppa/security/HashCash.java @@ -0,0 +1,480 @@ +package com.nettgryppa.security; +// Copyright 2006 Gregory Rubin grrubin@gmail.com +// Permission is given to use, modify, and or distribute this code so long as this message remains attached +// Please see the spec at: http://www.hashcash.org/ + +import java.util.*; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.security.NoSuchAlgorithmException; + +/** + * Class for generation and parsing of HashCash
+ * Copyright 2006 Gregory Rubin grrubin@gmail.com
+ * Permission is given to use, modify, and or distribute this code so long as this message remains attached
+ * Please see the spec at: http://www.hashcash.org/ + * @author grrubin@gmail.com + * @version 1.1 + */ +public class HashCash implements Comparable { + public static final int DefaultVersion = 1; + private static final int hashLength = 160; + private static final String dateFormatString = "yyMMdd"; + private static long milliFor16 = -1; + + private String myToken; + private int myValue; + private Calendar myDate; + private Map > myExtensions; + private int myVersion; + private String myResource; + // Constructors + + /** + * Parses and validates a HashCash. + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public HashCash(String cash) throws NoSuchAlgorithmException { + myToken = cash; + String[] parts = cash.split(":"); + myVersion = Integer.parseInt(parts[0]); + if(myVersion < 0 || myVersion > 1) + throw new IllegalArgumentException("Only supported versions are 0 and 1"); + + if((myVersion == 0 && parts.length != 6) || + (myVersion == 1 && parts.length != 7)) + throw new IllegalArgumentException("Improperly formed HashCash"); + + try { + int index = 1; + if(myVersion == 1) + myValue = Integer.parseInt(parts[index++]); + else + myValue = 0; + + SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatString); + Calendar tempCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + tempCal.setTime(dateFormat.parse(parts[index++])); + + myResource = parts[index++]; + myExtensions = deserializeExtensions(parts[index++]); + + MessageDigest md = MessageDigest.getInstance("SHA1"); + md.update(cash.getBytes()); + byte[] tempBytes = md.digest(); + int tempValue = numberOfLeadingZeros(tempBytes); + + if(myVersion == 0) + myValue = tempValue; + else if (myVersion == 1) + myValue = (tempValue > myValue ? myValue : tempValue); + } catch (java.text.ParseException ex) { + throw new IllegalArgumentException("Improperly formed HashCash", ex); + } + } + + private HashCash() throws NoSuchAlgorithmException { + } + + /** + * Mints a version 1 HashCash using now as the date + * @param resource the string to be encoded in the HashCash + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static HashCash mintCash(String resource, int value) throws NoSuchAlgorithmException { + Calendar now = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + return mintCash(resource, null, now, value, DefaultVersion); + } + + /** + * Mints a HashCash using now as the date + * @param resource the string to be encoded in the HashCash + * @param version Which version to mint. Only valid values are 0 and 1 + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static HashCash mintCash(String resource, int value, int version) throws NoSuchAlgorithmException { + Calendar now = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + return mintCash(resource, null, now, value, version); + } + + /** + * Mints a version 1 HashCash + * @param resource the string to be encoded in the HashCash + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static HashCash mintCash(String resource, Calendar date, int value) throws NoSuchAlgorithmException { + return mintCash(resource, null, date, value, DefaultVersion); + } + + /** + * Mints a HashCash + * @param resource the string to be encoded in the HashCash + * @param version Which version to mint. Only valid values are 0 and 1 + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static HashCash mintCash(String resource, Calendar date, int value, int version) + throws NoSuchAlgorithmException { + return mintCash(resource, null, date, value, version); + } + + /** + * Mints a version 1 HashCash using now as the date + * @param resource the string to be encoded in the HashCash + * @param extensions Extra data to be encoded in the HashCash + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static HashCash mintCash(String resource, Map > extensions, int value) + throws NoSuchAlgorithmException { + Calendar now = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + return mintCash(resource, extensions, now, value, DefaultVersion); + } + + /** + * Mints a HashCash using now as the date + * @param resource the string to be encoded in the HashCash + * @param extensions Extra data to be encoded in the HashCash + * @param version Which version to mint. Only valid values are 0 and 1 + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static HashCash mintCash(String resource, Map > extensions, int value, int version) + throws NoSuchAlgorithmException { + Calendar now = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + return mintCash(resource, extensions, now, value, version); + } + + /** + * Mints a version 1 HashCash + * @param resource the string to be encoded in the HashCash + * @param extensions Extra data to be encoded in the HashCash + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static HashCash mintCash(String resource, Map > extensions, Calendar date, int value) + throws NoSuchAlgorithmException { + return mintCash(resource, extensions, date, value, DefaultVersion); + } + + /** + * Mints a HashCash + * @param resource the string to be encoded in the HashCash + * @param extensions Extra data to be encoded in the HashCash + * @param version Which version to mint. Only valid values are 0 and 1 + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static HashCash mintCash(String resource, Map > extensions, Calendar date, int value, int version) + throws NoSuchAlgorithmException { + if(version < 0 || version > 1) + throw new IllegalArgumentException("Only supported versions are 0 and 1"); + + if(value < 0 || value > hashLength) + throw new IllegalArgumentException("Value must be between 0 and " + hashLength); + + if(resource.contains(":")) + throw new IllegalArgumentException("Resource may not contain a colon."); + + HashCash result = new HashCash(); + + MessageDigest md = MessageDigest.getInstance("SHA1"); + + result.myResource = resource; + result.myExtensions = (null == extensions ? new HashMap >() : extensions); + result.myDate = date; + result.myVersion = version; + + String prefix; + + SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatString); + switch(version) { + case 0: + prefix = version + ":" + dateFormat.format(date.getTime()) + ":" + resource + ":" + + serializeExtensions(extensions) + ":"; + result.myToken = generateCash(prefix, value, md); + md.reset(); + md.update(result.myToken.getBytes()); + result.myValue = numberOfLeadingZeros(md.digest()); + break; + + case 1: + result.myValue = value; + prefix = version + ":" + value + ":" + dateFormat.format(date.getTime()) + ":" + resource + ":" + + serializeExtensions(extensions) + ":"; + result.myToken = generateCash(prefix, value, md); + break; + + default: + throw new IllegalArgumentException("Only supported versions are 0 and 1"); + } + + return result; + } + + // Accessors + /** + * Two objects are considered equal if they are both of type HashCash and have an identical string representation + */ + public boolean equals(Object obj) { + if(obj instanceof HashCash) + return toString().equals(obj.toString()); + else + return super.equals(obj); + } + + /** + * Returns the canonical string representation of the HashCash + */ + public String toString() { + return myToken; + } + + /** + * Extra data encoded in the HashCash + */ + public Map > getExtensions() { + return myExtensions; + } + + /** + * The primary resource being protected + */ + public String getResource() { + return myResource; + } + + /** + * The minting date + */ + public Calendar getDate() { + return myDate; + } + + /** + * The value of the HashCash (e.g. how many leading zero bits it has) + */ + public int getValue() { + return myValue; + } + + /** + * Which version of HashCash is used here + */ + public int getVersion() { + return myVersion; + } + + // Private utility functions + /** + * Actually tries various combinations to find a valid hash. Form is of prefix + random_hex + ":" + random_hex + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + private static String generateCash(String prefix, int value, MessageDigest md) + throws NoSuchAlgorithmException { + SecureRandom rnd = SecureRandom.getInstance("SHA1PRNG"); + byte[] tmpBytes = new byte[8]; + rnd.nextBytes(tmpBytes); + long random = bytesToLong(tmpBytes); + rnd.nextBytes(tmpBytes); + long counter = bytesToLong(tmpBytes); + + prefix = prefix + Long.toHexString(random) + ":"; + + String temp; + int tempValue; + byte[] bArray; + do { + counter++; + temp = prefix + Long.toHexString(counter); + md.reset(); + md.update(temp.getBytes()); + bArray = md.digest(); + tempValue = numberOfLeadingZeros(bArray); + } while ( tempValue < value); + + return temp; + } + + /** + * Converts a 8 byte array of unsigned bytes to an long + * @param b an array of 8 unsigned bytes + */ +private static long bytesToLong(byte[] b) { + long l = 0; + l |= b[0] & 0xFF; + l <<= 8; + l |= b[1] & 0xFF; + l <<= 8; + l |= b[2] & 0xFF; + l <<= 8; + l |= b[3] & 0xFF; + l <<= 8; + l |= b[4] & 0xFF; + l <<= 8; + l |= b[5] & 0xFF; + l <<= 8; + l |= b[6] & 0xFF; + l <<= 8; + l |= b[7] & 0xFF; + return l; + } + + /** + * Serializes the extensions with (key, value) seperated by semi-colons and values seperated by commas + */ + private static String serializeExtensions(Map > extensions) { + if(null == extensions || extensions.isEmpty()) + return ""; + + StringBuffer result = new StringBuffer(); + List tempList; + boolean first = true; + + for(String key: extensions.keySet()) { + if(key.contains(":") || key.contains(";") || key.contains("=")) + throw new IllegalArgumentException("Extension key contains an illegal character. " + key); + if(!first) + result.append(";"); + first = false; + result.append(key); + tempList = extensions.get(key); + + if(null != tempList) { + result.append("="); + for(int i = 0; i < tempList.size(); i++) { + if(tempList.get(i).contains(":") || tempList.get(i).contains(";") || tempList.get(i).contains(",")) + throw new IllegalArgumentException("Extension value contains an illegal character. " + tempList.get(i)); + if(i > 0) + result.append(","); + result.append(tempList.get(i)); + } + } + } + return result.toString(); + } + + /** + * Inverse of {@link #serializeExtensions(Map)} + */ + private static Map > deserializeExtensions(String extensions) { + Map > result = new HashMap >(); + if(null == extensions || extensions.length() == 0) + return result; + + String[] items = extensions.split(";"); + + for(int i = 0; i < items.length; i++) { + String[] parts = items[i].split("=", 2); + if(parts.length == 1) + result.put(parts[0], null); + else + result.put(parts[0], Arrays.asList(parts[1].split(","))); + } + + return result; + } + + /** + * Counts the number of leading zeros in a byte array. + */ + private static int numberOfLeadingZeros(byte[] values) { + int result = 0; + int temp = 0; + for(int i = 0; i < values.length; i++) { + + temp = numberOfLeadingZeros(values[i]); + + result += temp; + if(temp != 8) + break; + } + + return result; + } + + /** + * Returns the number of leading zeros in a bytes binary represenation + */ + private static int numberOfLeadingZeros(byte value) { + if(value < 0) + return 0; + if(value < 1) + return 8; + else if (value < 2) + return 7; + else if (value < 4) + return 6; + else if (value < 8) + return 5; + else if (value < 16) + return 4; + else if (value < 32) + return 3; + else if (value < 64) + return 2; + else if (value < 128) + return 1; + else + return 0; + } + + /** + * Estimates how many milliseconds it would take to mint a cash of the specified value. + *
    + *
  • NOTE1: Minting time can vary greatly in fact, half of the time it will take half as long) + *
  • NOTE2: The first time that an estimation function is called it is expensive (on the order of seconds). After that, it is very quick. + *
+ * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static long estimateTime(int value) throws NoSuchAlgorithmException { + initEstimates(); + return (long)(milliFor16 * Math.pow(2, value - 16)); + } + + /** + * Estimates what value (e.g. how many bits of collision) are required for the specified length of time. + *
    + *
  • NOTE1: Minting time can vary greatly in fact, half of the time it will take half as long) + *
  • NOTE2: The first time that an estimation function is called it is expensive (on the order of seconds). After that, it is very quick. + *
+ * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + public static int estimateValue(int secs) throws NoSuchAlgorithmException { + initEstimates(); + int result = 0; + long millis = secs * 1000 * 65536; + millis /= milliFor16; + + while(millis > 1) { + result++; + millis /= 2; + } + + return result; + } + + /** + * Seeds the estimates by determining how long it takes to calculate a 16bit collision on average. + * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest + */ + private static void initEstimates() throws NoSuchAlgorithmException { + if(milliFor16 == -1) { + long duration; + duration = Calendar.getInstance().getTimeInMillis(); + for(int i = 0; i < 11; i++) { + mintCash("estimation", 16); + } + duration = Calendar.getInstance().getTimeInMillis() - duration; + milliFor16 = (duration /10); + } + } + + /** + * Compares the value of two HashCashes + * @param other + * @see java.lang.Comparable#compareTo(Object) + */ + public int compareTo(HashCash other) { + if(null == other) + throw new NullPointerException(); + + return Integer.valueOf(getValue()).compareTo(Integer.valueOf(other.getValue())); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/data/Certificate.java b/core/java/src/net/i2p/data/Certificate.java index 89a5aca97b..4932139fc2 100644 --- a/core/java/src/net/i2p/data/Certificate.java +++ b/core/java/src/net/i2p/data/Certificate.java @@ -36,6 +36,9 @@ public class Certificate extends DataStructureImpl { public final static int CERTIFICATE_TYPE_HASHCASH = 1; /** we should not be used for anything (don't use us in the netDb, in tunnels, or tell others about us) */ public final static int CERTIFICATE_TYPE_HIDDEN = 2; + /** Signed with 40-byte Signature and (optional) 32-byte hash */ + public final static int CERTIFICATE_TYPE_SIGNED = 3; + public final static int CERTIFICATE_LENGTH_SIGNED_WITH_HASH = Signature.SIGNATURE_BYTES + Hash.HASH_LENGTH; public Certificate() { _type = 0; @@ -149,19 +152,29 @@ public class Certificate extends DataStructureImpl { buf.append("Null certificate"); else if (getCertificateType() == CERTIFICATE_TYPE_HASHCASH) buf.append("Hashcash certificate"); + else if (getCertificateType() == CERTIFICATE_TYPE_HIDDEN) + buf.append("Hidden certificate"); + else if (getCertificateType() == CERTIFICATE_TYPE_SIGNED) + buf.append("Signed certificate"); else - buf.append("Unknown certificiate type (").append(getCertificateType()).append(")"); + buf.append("Unknown certificate type (").append(getCertificateType()).append(")"); if (_payload == null) { buf.append(" null payload"); } else { buf.append(" payload size: ").append(_payload.length); - int len = 32; - if (len > _payload.length) len = _payload.length; - buf.append(" first ").append(len).append(" bytes: "); - buf.append(DataHelper.toString(_payload, len)); + if (getCertificateType() == CERTIFICATE_TYPE_HASHCASH) { + buf.append(" Stamp: ").append(new String(_payload)); + } else if (getCertificateType() == CERTIFICATE_TYPE_SIGNED && _payload.length == CERTIFICATE_LENGTH_SIGNED_WITH_HASH) { + buf.append(" Signed by hash: ").append(Base64.encode(_payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH)); + } else { + int len = 32; + if (len > _payload.length) len = _payload.length; + buf.append(" first ").append(len).append(" bytes: "); + buf.append(DataHelper.toString(_payload, len)); + } } buf.append("]"); return buf.toString(); } -} \ No newline at end of file +} diff --git a/core/java/src/net/i2p/data/Destination.java b/core/java/src/net/i2p/data/Destination.java index cc647877bb..30589e2116 100644 --- a/core/java/src/net/i2p/data/Destination.java +++ b/core/java/src/net/i2p/data/Destination.java @@ -17,17 +17,17 @@ import java.io.OutputStream; import net.i2p.util.Log; /** - * Defines an end point in the I2P network. The Destination may move aroundn + * Defines an end point in the I2P network. The Destination may move around * in the network, but messages sent to the Destination will find it * * @author jrandom */ public class Destination extends DataStructureImpl { - private final static Log _log = new Log(Destination.class); - private Certificate _certificate; - private SigningPublicKey _signingKey; - private PublicKey _publicKey; - private Hash __calculatedHash; + protected final static Log _log = new Log(Destination.class); + protected Certificate _certificate; + protected SigningPublicKey _signingKey; + protected PublicKey _publicKey; + protected Hash __calculatedHash; public Destination() { setCertificate(null); @@ -174,4 +174,4 @@ public class Destination extends DataStructureImpl { } } } -} \ No newline at end of file +} diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java index 85e7614505..edb4d8ceea 100644 --- a/core/java/src/net/i2p/data/PrivateKeyFile.java +++ b/core/java/src/net/i2p/data/PrivateKeyFile.java @@ -1,39 +1,79 @@ package net.i2p.data; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.util.Iterator; +import java.util.Map; import java.util.Properties; +import com.nettgryppa.security.HashCash; + import net.i2p.I2PException; import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; +import net.i2p.crypto.DSAEngine; +/** + * This helper class reads and writes files in the + * same "eepPriv.dat" format used by the client code. + * The format is: + * - Destination (387 bytes if no certificate, otherwise longer) + * - Public key (256 bytes) + * - Signing Public key (128 bytes) + * - Cert. type (1 byte) + * - Cert. length (2 bytes) + * - Certificate if length != 0 + * - Private key (256 bytes) + * - Signing Private key (20 bytes) + * Total 663 bytes + * + * @author welterde, zzz + */ public class PrivateKeyFile { public PrivateKeyFile(File file, I2PClient client) { this.file = file; this.client = client; this.dest = null; + this.privKey = null; + this.signingPrivKey = null; } - public void createIfAbsent() throws I2PException, IOException { + /** Also reads in the file to get the privKay and signingPrivKey, + * which aren't available from I2PClient. + */ + public Destination createIfAbsent() throws I2PException, IOException, DataFormatException { if(!this.file.exists()) { FileOutputStream out = new FileOutputStream(this.file); - this.dest = this.client.createDestination(out); + this.client.createDestination(out); out.close(); } + return getDestination(); } - public Destination getDestination() { - // TODO: how to load destination if this is an old key? - return dest; + /** Also sets the local privKay and signingPrivKey */ + public Destination getDestination() throws I2PSessionException, IOException, DataFormatException { + if (dest == null) { + I2PSession s = open(); + if (s != null) { + this.dest = new VerifiedDestination(s.getMyDestination()); + this.privKey = s.getDecryptionKey(); + this.signingPrivKey = s.getPrivateKey(); + } + } + return this.dest; } + public PrivateKey getPrivKey() { return this.privKey; } + public SigningPrivateKey getSigningPrivKey() { return this.signingPrivKey; } + public I2PSession open() throws I2PSessionException, IOException { return this.open(new Properties()); } @@ -50,10 +90,205 @@ public class PrivateKeyFile { return s; } - - - + /** + * Copied from I2PClientImpl.createDestination() + */ + public void write() throws IOException, DataFormatException { + FileOutputStream out = new FileOutputStream(this.file); + this.dest.writeBytes(out); + this.privKey.writeBytes(out); + this.signingPrivKey.writeBytes(out); + out.flush(); + out.close(); + } + + public String toString() { + StringBuffer s = new StringBuffer(128); + s.append("Dest: "); + s.append(this.dest.toBase64()); + s.append("\nContains: "); + s.append(this.dest); + s.append("\nPrivate Key: "); + s.append(this.privKey); + s.append("\nSigining Private Key: "); + s.append(this.signingPrivKey); + s.append("\n"); + return s.toString(); + } + private File file; private I2PClient client; private Destination dest; -} \ No newline at end of file + private PrivateKey privKey; + private SigningPrivateKey signingPrivKey; + + private static final int HASH_EFFORT = VerifiedDestination.MIN_HASHCASH_EFFORT; + + /** + * Create a new PrivateKeyFile, or modify an existing one, with various + * types of Certificates. + * + * Changing a Certificate does not change the public or private keys. + * But it does change the Destination Hash, which effectively makes it + * a new Destination. In other words, don't change the Certificate on + * a Destination you've already registered in a hosts.txt key add form. + * + * Copied and expanded from that in Destination.java + */ + public static void main(String args[]) { + if (args.length == 0) { + System.err.println("Usage: PrivateKeyFile filename (generates if nonexistent, then prints)"); + System.err.println(" PrivateKeyFile -h filename (generates if nonexistent, adds hashcash cert)"); + System.err.println(" PrivateKeyFile -h effort filename (specify HashCash effort instead of default " + HASH_EFFORT + ")"); + System.err.println(" PrivateKeyFile -n filename (changes to null cert)"); + System.err.println(" PrivateKeyFile -s filename signwithdestfile (generates if nonexistent, adds cert signed by 2nd dest)"); + System.err.println(" PrivateKeyFile -u filename (changes to unknown cert)"); + System.err.println(" PrivateKeyFile -x filename (changes to hidden cert)"); + return; + } + I2PClient client = I2PClientFactory.createClient(); + + int filearg = 0; + if (args.length > 1) { + if (args.length >= 2 && args[0].equals("-h")) + filearg = args.length - 1; + else + filearg = 1; + } + try { + File f = new File(args[filearg]); + PrivateKeyFile pkf = new PrivateKeyFile(f, client); + Destination d = pkf.createIfAbsent(); + System.out.println("Original Destination:"); + System.out.println(pkf); + verifySignature(d); + if (args.length == 1) + return; + Certificate c = new Certificate(); + if (args[0].equals("-n")) { + // Cert constructor generates a null cert + } else if (args[0].equals("-u")) { + c.setCertificateType(99); + } else if (args[0].equals("-x")) { + c.setCertificateType(Certificate.CERTIFICATE_TYPE_HIDDEN); + } else if (args[0].equals("-h")) { + int hashEffort = HASH_EFFORT; + if (args.length == 3) + hashEffort = Integer.parseInt(args[1]); + System.out.println("Estimating hashcash generation time, stand by..."); + // takes a lot longer than the estimate usually... + // maybe because the resource string is much longer than used in the estimate? + long low = HashCash.estimateTime(hashEffort); + System.out.println("It is estimated this will take " + DataHelper.formatDuration(low) + + " to " + DataHelper.formatDuration(4*low)); + + long begin = System.currentTimeMillis(); + System.out.println("Starting hashcash generation now..."); + String resource = d.getPublicKey().toBase64() + d.getSigningPublicKey().toBase64(); + HashCash hc = HashCash.mintCash(resource, hashEffort); + System.out.println("Generation took: " + DataHelper.formatDuration(System.currentTimeMillis() - begin)); + System.out.println("Full Hashcash is: " + hc); + // Take the resource out of the stamp + String hcs = hc.toString(); + int end1 = 0; + for (int i = 0; i < 3; i++) { + end1 = 1 + hcs.indexOf(':', end1); + if (end1 < 0) { + System.out.println("Bad hashcash"); + return; + } + } + int start2 = hcs.indexOf(':', end1); + if (start2 < 0) { + System.out.println("Bad hashcash"); + return; + } + hcs = hcs.substring(0, end1) + hcs.substring(start2); + System.out.println("Short Hashcash is: " + hcs); + + c.setCertificateType(Certificate.CERTIFICATE_TYPE_HASHCASH); + c.setPayload(hcs.getBytes()); + } else if (args.length == 3 && args[0].equals("-s")) { + // Sign dest1 with dest2's Signing Private Key + File f2 = new File(args[2]); + I2PClient client2 = I2PClientFactory.createClient(); + PrivateKeyFile pkf2 = new PrivateKeyFile(f2, client2); + Destination d2 = pkf2.getDestination(); + SigningPrivateKey spk2 = pkf2.getSigningPrivKey(); + System.out.println("Signing With Dest:"); + System.out.println(pkf2.toString()); + + int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert + byte[] data = new byte[len]; + System.arraycopy(d.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); + System.arraycopy(d.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); + byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES]; + byte[] sig = DSAEngine.getInstance().sign(new ByteArrayInputStream(data), spk2).getData(); + System.arraycopy(sig, 0, payload, 0, Signature.SIGNATURE_BYTES); + // Add dest2's Hash for reference + byte[] h2 = d2.calculateHash().getData(); + System.arraycopy(h2, 0, payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH); + c.setCertificateType(Certificate.CERTIFICATE_TYPE_SIGNED); + c.setPayload(payload); + } + d.setCertificate(c); // do this rather than just change the existing cert so the hash is recalculated + System.out.println("New signed destination is:"); + System.out.println(pkf); + pkf.write(); + verifySignature(d); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Sample code to verify a 3rd party signature. + * This just goes through all the hosts.txt files and tries everybody. + * You need to be in the $I2P directory or have a local hosts.txt for this to work. + * Doubt this is what you want as it is super-slow, and what good is + * a signing scheme where anybody is allowed to sign? + * + * In a real application you would make a list of approved signers, + * do a naming lookup to get their Destinations, and try those only. + * Or do a netDb lookup of the Hash in the Certificate, do a reverse + * naming lookup to see if it is allowed, then verify the Signature. + */ + public static boolean verifySignature(Destination d) { + if (d.getCertificate().getCertificateType() != Certificate.CERTIFICATE_TYPE_SIGNED) + return false; + int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert + byte[] data = new byte[len]; + System.arraycopy(d.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); + System.arraycopy(d.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); + Signature sig = new Signature(d.getCertificate().getPayload()); + + String[] filenames = new String[] {"privatehosts.txt", "userhosts.txt", "hosts.txt"}; + for (int i = 0; i < filenames.length; i++) { + Properties hosts = new Properties(); + try { + File f = new File(filenames[i]); + if ( (f.exists()) && (f.canRead()) ) { + DataHelper.loadProps(hosts, f, true); + + for (Iterator iter = hosts.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry entry = (Map.Entry)iter.next(); + String s = (String) entry.getValue(); + Destination signer = new Destination(s); + if (checkSignature(sig, data, signer.getSigningPublicKey())) { + System.out.println("Good signature from: " + entry.getKey()); + return true; + } + } + } + } catch (Exception ioe) { + } + // not found, continue to the next file + } + System.out.println("No valid signer found"); + return false; + } + + public static boolean checkSignature(Signature s, byte[] data, SigningPublicKey spk) { + return DSAEngine.getInstance().verifySignature(s, data, spk); + } +} diff --git a/core/java/src/net/i2p/data/VerifiedDestination.java b/core/java/src/net/i2p/data/VerifiedDestination.java new file mode 100644 index 0000000000..4e90b8cdff --- /dev/null +++ b/core/java/src/net/i2p/data/VerifiedDestination.java @@ -0,0 +1,163 @@ +package net.i2p.data; + +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.security.NoSuchAlgorithmException; + +import com.nettgryppa.security.HashCash; + +import net.i2p.util.Log; + +/** + * Extend Destination with methods to verify its Certificate. + * The router does not check Certificates, it doesn't care. + * Apps however (particularly addressbook) may wish to enforce various + * cert content, format, and policies. + * This class is written such that apps may extend it to + * create their own policies. + * + * @author zzz + */ +public class VerifiedDestination extends Destination { + protected final static Log _log = new Log(Destination.class); + + public VerifiedDestination() { + super(); + } + + /** + * alternative constructor which takes a base64 string representation + * @param s a Base64 representation of the destination, as (eg) is used in hosts.txt + */ + public VerifiedDestination(String s) throws DataFormatException { + this(); + fromBase64(s); + } + + /** + * create from an existing Dest + * @param d must be non-null + */ + public VerifiedDestination(Destination d) throws DataFormatException { + this(d.toBase64()); + } + + /** + * verify the certificate. + * @param allowNone If true, allow a NULL or HIDDEN certificate. + */ + public boolean verifyCert(boolean allowNone) { + if (_publicKey == null || _signingKey == null || _certificate == null) + return false; + switch (_certificate.getCertificateType()) { + case Certificate.CERTIFICATE_TYPE_NULL: + case Certificate.CERTIFICATE_TYPE_HIDDEN: + return allowNone; + case Certificate.CERTIFICATE_TYPE_HASHCASH: + return verifyHashCashCert(); + case Certificate.CERTIFICATE_TYPE_SIGNED: + return verifySignedCert(); + } + return verifyUnknownCert(); + } + + /** Defaults for HashCash Certs */ + public final static int MIN_HASHCASH_EFFORT = 20; + + /** + * HashCash Certs are used to demonstrate proof-of-work. + * + * We define a HashCash Certificate as follows: + * - length: typically 47 bytes, but may vary somewhat + * - contents: A version 1 HashCash Stamp, + * defined at http://www.hashcash.org/docs/hashcash.html#stamp_format__version_1_ + * modified to remove the contents of the 4th field (the resource) + * original is ver:bits:date:resource:[ext]:rand:counter + * I2P version is ver:bits:date::[ext]:rand:counter + * The HashCash is calculated with the following resource: + * The Base64 of the Public Key concatenated with the Base64 of the Signing Public Key + * (NOT the Base64 of the concatenated keys) + * To generate a Cert of this type, see PrivateKeyFile.main() + * To verify, we must put the keys back into the resource field of the stamp, + * then pass it to the HashCash constructor, then get the number of leading + * zeros and see if it meets our minimum effort. + */ + protected boolean verifyHashCashCert() { + String hcs = new String(_certificate.getPayload()); + int end1 = 0; + for (int i = 0; i < 3; i++) { + end1 = 1 + hcs.indexOf(':', end1); + if (end1 < 0) + return false; + } + int start2 = hcs.indexOf(':', end1); + if (start2 < 0) + return false; + // put the keys back into the 4th field of the stamp + hcs = hcs.substring(0, end1) + _publicKey.toBase64() + _signingKey.toBase64() + hcs.substring(start2); + HashCash hc; + try { + hc = new HashCash(hcs); + } catch (IllegalArgumentException iae) { + return false; + } catch (NoSuchAlgorithmException nsae) { + return false; + } + return hc.getValue() >= MIN_HASHCASH_EFFORT; + } + + /** Defaults for Signed Certs */ + public final static int CERTIFICATE_LENGTH_SIGNED = Signature.SIGNATURE_BYTES; + public final static int CERTIFICATE_LENGTH_SIGNED_WITH_HASH = Signature.SIGNATURE_BYTES + Hash.HASH_LENGTH; + + /** + * Signed Certs are signed by a 3rd-party Destination. + * They can be used for a second-level domain, for example, to sign the + * Destination for a third-level domain. Or for a central authority + * to approve a destination. + * + * We define a Signed Certificate as follows: + * - length: Either 44 or 72 bytes + * - contents: + * 1: a 44 byte Signature + * 2 (optional): a 32 byte Hash of the signing Destination + * This can be a hint to the verification process to help find + * the identity and keys of the signing Destination. + * Data which is signed: The first 384 bytes of the Destination + * (i.e. the Public Key and Signing Public Key, WITHOUT the Certificate) + * + * It is not appropriate to enforce a particular delegation scheme here. + * The application will need to apply additional steps to select + * an appropriate signing Destination and verify the signature. + * + * See PrivateKeyFile.verifySignature() for sample verification code. + * + */ + protected boolean verifySignedCert() { + return _certificate.getPayload() != null && + (_certificate.getPayload().length == CERTIFICATE_LENGTH_SIGNED || + _certificate.getPayload().length == CERTIFICATE_LENGTH_SIGNED_WITH_HASH); + } + + /** + * Reject all unknown certs + */ + protected boolean verifyUnknownCert() { + return false; + } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append(super.toString()); + buf.append("\n\tVerified Certificate? ").append(verifyCert(true)); + return buf.toString(); + } + +} diff --git a/history.txt b/history.txt index 16901547a6..1410468005 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,22 @@ -2008-10-27 zzz +2008-11-02 zzz + * Certificates: + - Add a signed Certificate type + - Add a main() to PrivateKeyFile to generate + Destinations with various Certificate types + - Add a VerifiedDestination class to check Certificates + of various types + - Add a HashCash library from http://www.nettgryppa.com/code/ + (no distribution restrictions) + - Allow non-null Certificates in addressbook + * I2PTunnel: Move some wayward stats to the I2PTunnel group + * NamingServices: Implement caching in the abstract class + * NewsFetcher: Fix last updated time + * Streaming: Increase MTU to 1730 (was 960); + see ConnectionOptions.java for analysis + * Throttle: Reduce default max tunnels to 2000 (was 2500) + * clients.config: Disable SAM and BOB by default for new installs + +2008-10-26 zzz * config.jsp: Add more help * peers.jsp: Clean up 'Listening on' formatting * profiles.jsp: Don't override locale number format diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 2f0d4e9d01..56124cd55a 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.6.4"; - public final static long BUILD = 7; + public final static long BUILD = 8; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID);