* 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:
@ -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
|
||||
;
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ div.routersummary {
|
||||
color: inherit;
|
||||
font-size: small;
|
||||
clear: left; /* fixes a bug in Opera */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
|
480
core/java/src/com/nettgryppa/security/HashCash.java
Normal file
480
core/java/src/com/nettgryppa/security/HashCash.java
Normal 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()));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
163
core/java/src/net/i2p/data/VerifiedDestination.java
Normal file
163
core/java/src/net/i2p/data/VerifiedDestination.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
20
history.txt
20
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
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user