* 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
This commit is contained in:
zzz
2008-11-02 22:13:11 +00:00
parent 47d5e44b16
commit bf12c5f9bf
9 changed files with 939 additions and 26 deletions

View File

@ -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
;
}

View File

@ -45,6 +45,7 @@ div.routersummary {
color: inherit;
font-size: small;
clear: left; /* fixes a bug in Opera */
overflow: auto;
}
div.warning {

View File

@ -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 <a href="http://www.hashcash.org/">HashCash</a><br>
* Copyright 2006 Gregory Rubin <a href="mailto:grrubin@gmail.com">grrubin@gmail.com</a><br>
* Permission is given to use, modify, and or distribute this code so long as this message remains attached<br>
* Please see the spec at: <a href="http://www.hashcash.org/">http://www.hashcash.org/</a>
* @author grrubin@gmail.com
* @version 1.1
*/
public class HashCash implements Comparable<HashCash> {
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<String, List<String> > 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<String, List<String> > 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<String, List<String> > 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<String, List<String> > 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<String, List<String> > 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<String, List<String> >() : 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<String, List<String> > 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<String, List<String> > extensions) {
if(null == extensions || extensions.isEmpty())
return "";
StringBuffer result = new StringBuffer();
List<String> 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<String, List<String> > deserializeExtensions(String extensions) {
Map<String, List<String> > result = new HashMap<String, List<String> >();
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.
* <ul>
* <li>NOTE1: Minting time can vary greatly in fact, half of the time it will take half as long)
* <li>NOTE2: The first time that an estimation function is called it is expensive (on the order of seconds). After that, it is very quick.
* </ul>
* @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.
* <ul>
* <li>NOTE1: Minting time can vary greatly in fact, half of the time it will take half as long)
* <li>NOTE2: The first time that an estimation function is called it is expensive (on the order of seconds). After that, it is very quick.
* </ul>
* @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()));
}
}

View File

@ -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();
}
}
}

View File

@ -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 {
}
}
}
}
}

View File

@ -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;
}
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);
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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);