I2CP, Data: Initial support for LS2 offline keys in I2PSession and PrivateKeyFile

This commit is contained in:
zzz
2018-12-04 20:59:38 +00:00
parent 177f595f33
commit 2876da2565
5 changed files with 337 additions and 19 deletions

View File

@ -19,7 +19,9 @@ import net.i2p.data.Hash;
import net.i2p.data.PrivateKey; import net.i2p.data.PrivateKey;
import net.i2p.data.SessionKey; import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag; import net.i2p.data.SessionTag;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
/** /**
* <p>Define the standard means of sending and receiving messages on the * <p>Define the standard means of sending and receiving messages on the
@ -300,10 +302,36 @@ public interface I2PSession {
public PrivateKey getDecryptionKey(); public PrivateKey getDecryptionKey();
/** /**
* Retrieve the signing SigningPrivateKey associated with the Destination * Retrieve the signing SigningPrivateKey associated with the Destination.
* As of 0.9.38, this will be the transient key if offline signed.
*/ */
public SigningPrivateKey getPrivateKey(); public SigningPrivateKey getPrivateKey();
/**
* Does this session have offline and transient keys?
* @since 0.9.38
*/
public boolean isOffline();
/**
* Get the offline expiration
* @return Java time (ms) or 0 if not initialized or does not have offline keys
* @since 0.9.38
*/
public long getOfflineExpiration();
/**
* @return null on error or if not initialized or does not have offline keys
* @since 0.9.38
*/
public Signature getOfflineSignature();
/**
* @return null on error or if not initialized or does not have offline keys
* @since 0.9.38
*/
public SigningPublicKey getTransientSigningPublicKey();
/** /**
* Lookup a Destination by Hash. * Lookup a Destination by Hash.
* Blocking. Waits a max of 10 seconds by default. * Blocking. Waits a max of 10 seconds by default.

View File

@ -36,13 +36,17 @@ import net.i2p.client.I2PClient;
import net.i2p.client.I2PSession; import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException; import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionListener; import net.i2p.client.I2PSessionListener;
import net.i2p.crypto.SigType;
import net.i2p.data.Base32; import net.i2p.data.Base32;
import net.i2p.data.DataFormatException; import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination; import net.i2p.data.Destination;
import net.i2p.data.Hash; import net.i2p.data.Hash;
import net.i2p.data.LeaseSet; import net.i2p.data.LeaseSet;
import net.i2p.data.PrivateKey; import net.i2p.data.PrivateKey;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.i2cp.DestLookupMessage; import net.i2p.data.i2cp.DestLookupMessage;
import net.i2p.data.i2cp.DestReplyMessage; import net.i2p.data.i2cp.DestReplyMessage;
import net.i2p.data.i2cp.GetBandwidthLimitsMessage; import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
@ -91,6 +95,9 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
private SessionId _sessionId; private SessionId _sessionId;
/** currently granted lease set, or null */ /** currently granted lease set, or null */
protected volatile LeaseSet _leaseSet; protected volatile LeaseSet _leaseSet;
private long _offlineExpiration;
private Signature _offlineSignature;
protected SigningPublicKey _transientSigningPublicKey;
// subsession stuff // subsession stuff
// registered subsessions // registered subsessions
@ -301,6 +308,9 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
* *
* As of 0.9.19, defaults in options are honored. * As of 0.9.19, defaults in options are honored.
* *
* This does NOT validate consistency of the destKeyStream,
* e.g. pubkey/privkey match or valid offline sig. The router does that.
*
* @param destKeyStream stream containing the private key data, * @param destKeyStream stream containing the private key data,
* format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile} * format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
* @param options set of options to configure the router with, if null will use System properties * @param options set of options to configure the router with, if null will use System properties
@ -548,7 +558,11 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
} }
/** /**
* Load up the destKeyFile for our Destination, PrivateKey, and SigningPrivateKey * Load up the destKeyFile for our Destination, PrivateKey, and SigningPrivateKey.
* As of 0.9.38, loads the offline data also. See PrivateKeyFile.
*
* This does NOT validate consistency of the destKeyStream,
* e.g. pubkey/privkey match or valid offline sig. The router does that.
* *
* @throws DataFormatException if the file is in the wrong format or keys are invalid * @throws DataFormatException if the file is in the wrong format or keys are invalid
* @throws IOException if there is a problem reading the file * @throws IOException if there is a problem reading the file
@ -556,8 +570,68 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
private void readDestination(InputStream destKeyStream) throws DataFormatException, IOException { private void readDestination(InputStream destKeyStream) throws DataFormatException, IOException {
_myDestination.readBytes(destKeyStream); _myDestination.readBytes(destKeyStream);
_privateKey.readBytes(destKeyStream); _privateKey.readBytes(destKeyStream);
_signingPrivateKey = new SigningPrivateKey(_myDestination.getSigningPublicKey().getType()); SigType dtype = _myDestination.getSigningPublicKey().getType();
_signingPrivateKey = new SigningPrivateKey(dtype);
_signingPrivateKey.readBytes(destKeyStream); _signingPrivateKey.readBytes(destKeyStream);
if (isOffline(_signingPrivateKey)) {
_offlineExpiration = DataHelper.readLong(destKeyStream, 4) * 1000;;
int itype = (int) DataHelper.readLong(destKeyStream, 2);
SigType type = SigType.getByCode(itype);
if (type == null)
throw new DataFormatException("Unsupported transient sig type: " + itype);
_transientSigningPublicKey = new SigningPublicKey(type);
_transientSigningPublicKey.readBytes(destKeyStream);
_offlineSignature = new Signature(dtype);
_offlineSignature.readBytes(destKeyStream);
// replace SPK
_signingPrivateKey = new SigningPrivateKey(type);
_signingPrivateKey.readBytes(destKeyStream);
}
}
/**
* Constant time
* @since 0.9.38
*/
private static boolean isOffline(SigningPrivateKey spk) {
byte b = 0;
byte[] data = spk.getData();
for (int i = 0; i < data.length; i++) {
b |= data[i];
}
return b == 0;
}
/**
* Does this session have offline and transient keys?
* @since 0.9.38
*/
public boolean isOffline() {
return _offlineSignature != null;
}
/**
* @return Java time (ms) or 0 if not initialized or does not have offline keys
* @since 0.9.38
*/
public long getOfflineExpiration() {
return _offlineExpiration;
}
/**
* @return null on error or if not initialized or does not have offline keys
* @since 0.9.38
*/
public Signature getOfflineSignature() {
return _offlineSignature;
}
/**
* @return null on error or if not initialized or does not have offline keys
* @since 0.9.38
*/
public SigningPublicKey getTransientSigningPublicKey() {
return _transientSigningPublicKey;
} }
/** /**
@ -1044,7 +1118,8 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
public PrivateKey getDecryptionKey() { return _privateKey; } public PrivateKey getDecryptionKey() { return _privateKey; }
/** /**
* Retrieve the signing SigningPrivateKey * Retrieve the signing SigningPrivateKey.
* As of 0.9.38, this will be the transient key if offline signed.
*/ */
public SigningPrivateKey getPrivateKey() { return _signingPrivateKey; } public SigningPrivateKey getPrivateKey() { return _signingPrivateKey; }

View File

@ -11,6 +11,7 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
@ -45,6 +46,14 @@ import net.i2p.util.SecureFileOutputStream;
* - Certificate if length != 0 * - Certificate if length != 0
* - Private key (256 bytes) * - Private key (256 bytes)
* - Signing Private key (20 bytes, or length specified by key certificate) * - Signing Private key (20 bytes, or length specified by key certificate)
* - As of 0.9.38, if the Signing Private Key is all zeros,
* the offline signature section (see proposal 123):
* - Expires timestamp (4 bytes, seconds since epoch, rolls over in 2106)
* - Sig type of transient public key (2 bytes)
* - Transient Signing Public key (length as specified by transient sig type)
* - Signature of Signed Public key by offline key (length as specified by destination sig type)
* - Transient Signing Private key (length as specified by transient sig type)
*
* Total: 663 or more bytes * Total: 663 or more bytes
*</pre> *</pre>
* *
@ -60,6 +69,10 @@ public class PrivateKeyFile {
protected Destination dest; protected Destination dest;
protected PrivateKey privKey; protected PrivateKey privKey;
protected SigningPrivateKey signingPrivKey; protected SigningPrivateKey signingPrivKey;
private long _offlineExpiration;
private Signature _offlineSignature;
private SigningPrivateKey _transientSigningPrivKey;
private SigningPublicKey _transientSigningPubKey;
/** /**
* Create a new PrivateKeyFile, or modify an existing one, with various * Create a new PrivateKeyFile, or modify an existing one, with various
@ -75,10 +88,13 @@ public class PrivateKeyFile {
public static void main(String args[]) { public static void main(String args[]) {
int hashEffort = HASH_EFFORT; int hashEffort = HASH_EFFORT;
String stype = null; String stype = null;
String ttype = null;
String hostname = null; String hostname = null;
String offline = null;
int days = 365;
int mode = 0; int mode = 0;
boolean error = false; boolean error = false;
Getopt g = new Getopt("pkf", args, "t:nuxhse:c:a:"); Getopt g = new Getopt("pkf", args, "t:nuxhse:c:a:o:d:r:");
int c; int c;
while ((c = g.getopt()) != -1) { while ((c = g.getopt()) != -1) {
switch (c) { switch (c) {
@ -109,10 +125,26 @@ public class PrivateKeyFile {
error = true; error = true;
break; break;
case 'o':
offline = g.getOptarg();
if (mode == 0)
mode = c;
else
error = true;
break;
case 'e': case 'e':
hashEffort = Integer.parseInt(g.getOptarg()); hashEffort = Integer.parseInt(g.getOptarg());
break; break;
case 'd':
days = Integer.parseInt(g.getOptarg());
break;
case 'r':
ttype = g.getOptarg();
break;
case '?': case '?':
case ':': case ':':
default: default:
@ -132,7 +164,8 @@ public class PrivateKeyFile {
I2PClient client = I2PClientFactory.createClient(); I2PClient client = I2PClientFactory.createClient();
try { try {
File f = new File(filearg); String orig = offline != null ? offline : filearg;
File f = new File(orig);
boolean exists = f.exists(); boolean exists = f.exists();
PrivateKeyFile pkf = new PrivateKeyFile(f, client); PrivateKeyFile pkf = new PrivateKeyFile(f, client);
Destination d; Destination d;
@ -179,13 +212,16 @@ public class PrivateKeyFile {
break; break;
case 's': case 's':
{
// Sign dest1 with dest2's Signing Private Key // Sign dest1 with dest2's Signing Private Key
PrivateKeyFile pkf2 = new PrivateKeyFile(args[g.getOptind() + 1]); PrivateKeyFile pkf2 = new PrivateKeyFile(args[g.getOptind() + 1]);
pkf.setSignedCert(pkf2); pkf.setSignedCert(pkf2);
System.out.println("New destination with signed cert is:"); System.out.println("New destination with signed cert is:");
break; break;
}
case 't': case 't':
{
// KeyCert // KeyCert
SigType type = SigType.parseSigType(stype); SigType type = SigType.parseSigType(stype);
if (type == null) if (type == null)
@ -193,8 +229,10 @@ public class PrivateKeyFile {
pkf.setKeyCert(type); pkf.setKeyCert(type);
System.out.println("New destination with key cert is:"); System.out.println("New destination with key cert is:");
break; break;
}
case 'a': case 'a':
{
// addressbook auth // addressbook auth
OrderedProperties props = new OrderedProperties(); OrderedProperties props = new OrderedProperties();
HostTxtEntry he = new HostTxtEntry(hostname, d.toBase64(), props); HostTxtEntry he = new HostTxtEntry(hostname, d.toBase64(), props);
@ -205,6 +243,50 @@ public class PrivateKeyFile {
out.flush(); out.flush();
System.out.println(""); System.out.println("");
return; return;
}
case 'o':
{
// Sign dest1 with dest2's Signing Private Key
File f3 = new File(filearg);
// set dummy SPK
SigType type = pkf.getSigningPrivKey().getType();
byte[] dbytes = new byte[type.getPrivkeyLen()];
SigningPrivateKey dummy = new SigningPrivateKey(type, dbytes);
PrivateKeyFile pkf2 = new PrivateKeyFile(f3, pkf.getDestination(), pkf.getPrivKey(), dummy);
// keygen transient
SigType tstype = SigType.EdDSA_SHA512_Ed25519;
if (ttype != null) {
tstype = SigType.parseSigType(ttype);
if (tstype == null)
throw new I2PException("Bad or unsupported -r option: " + ttype);
}
SimpleDataStructure signingKeys[];
try {
signingKeys = KeyGenerator.getInstance().generateSigningKeys(tstype);
} catch (GeneralSecurityException gse) {
throw new RuntimeException("keygen fail", gse);
}
SigningPublicKey tSigningPubKey = (SigningPublicKey) signingKeys[0];
SigningPrivateKey tSigningPrivKey = (SigningPrivateKey) signingKeys[1];
// set expires
long expires = System.currentTimeMillis() + (days * 24*60*60*1000L);
// sign
byte[] data = new byte[4 + 2 + tSigningPubKey.length()];
DataHelper.toLong(data, 0, 4, expires / 1000);
DataHelper.toLong(data, 4, 2, tstype.getCode());
System.arraycopy(tSigningPubKey.getData(), 0, data, 6, tSigningPubKey.length());
Signature sign = DSAEngine.getInstance().sign(data, pkf.getSigningPrivKey());
if (sign == null)
throw new I2PException("Sig fail");
// set the offline block
pkf2.setOfflineData(expires, tSigningPubKey, sign, tSigningPrivKey);
// write it
System.out.println("New destination with offline signature is:");
System.out.println(pkf2);
pkf2.write();
return;
}
default: default:
// shouldn't happen // shouldn't happen
@ -226,15 +308,22 @@ public class PrivateKeyFile {
} }
private static void usage() { private static void usage() {
System.err.println("Usage: PrivateKeyFile [-c sigtype] filename (generates if nonexistent, then prints)\n" + System.err.println("Usage: PrivateKeyFile filename (generates if nonexistent, then prints)\n" +
" PrivateKeyFile -a example.i2p filename (generate addressbook authentication string)\n" + " \ncertificate options:\n" +
" PrivateKeyFile -h filename (generates if nonexistent, adds hashcash cert)\n" + " -h (generates if nonexistent, adds hashcash cert)\n" +
" PrivateKeyFile -h -e effort filename (specify HashCash effort instead of default " + HASH_EFFORT + ")\n" + " -n (changes to null cert)\n" +
" PrivateKeyFile -n filename (changes to null cert)\n" + " -s signwithdestfile (generates if nonexistent, adds cert signed by 2nd dest)\n" +
" PrivateKeyFile -s filename signwithdestfile (generates if nonexistent, adds cert signed by 2nd dest)\n" + " -u (changes to unknown cert)\n" +
" PrivateKeyFile -t sigtype filename (changes to KeyCertificate of the given sig type)\n" + " -x (changes to hidden cert)\n" +
" PrivateKeyFile -u filename (changes to unknown cert)\n" + " \nother options:\n" +
" PrivateKeyFile -x filename (changes to hidden cert)\n"); " -a example.i2p (generate addressbook authentication string)\n" +
" -d days (specify expiration in days of offline sig, default 365)\n" +
" -c sigtype (specify sig type of destination)\n" +
" -e effort (specify HashCash effort instead of default " + HASH_EFFORT + ")\n" +
" -o offlinedestfile (generate the online key file using the offline key file specified)\n" +
" -r sigtype (specify sig type of transient key, default Ed25519)\n" +
" -t sigtype (changes to KeyCertificate of the given sig type)\n" +
"");
} }
public PrivateKeyFile(String file) { public PrivateKeyFile(String file) {
@ -358,6 +447,16 @@ public class PrivateKeyFile {
this.dest = new VerifiedDestination(s.getMyDestination()); this.dest = new VerifiedDestination(s.getMyDestination());
this.privKey = s.getDecryptionKey(); this.privKey = s.getDecryptionKey();
this.signingPrivKey = s.getPrivateKey(); this.signingPrivKey = s.getPrivateKey();
if (s.isOffline()) {
_offlineExpiration = s.getOfflineExpiration();
_transientSigningPubKey = s.getTransientSigningPublicKey();
_offlineSignature = s.getOfflineSignature();
_transientSigningPrivKey = signingPrivKey;
// set dummy SPK
SigType type = dest.getSigningPublicKey().getType();
byte[] dbytes = new byte[type.getPrivkeyLen()];
signingPrivKey = new SigningPrivateKey(type, dbytes);
}
} }
} }
return this.dest; return this.dest;
@ -480,7 +579,7 @@ public class PrivateKeyFile {
System.arraycopy(this.dest.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); System.arraycopy(this.dest.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES);
System.arraycopy(this.dest.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); System.arraycopy(this.dest.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES);
byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES]; byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES];
Signature sign = DSAEngine.getInstance().sign(new ByteArrayInputStream(data), spk2); Signature sign = DSAEngine.getInstance().sign(data, spk2);
if (sign == null) if (sign == null)
return null; return null;
byte[] sig = sign.getData(); byte[] sig = sign.getData();
@ -519,6 +618,91 @@ public class PrivateKeyFile {
return this.signingPrivKey; return this.signingPrivKey;
} }
//// offline methods
/**
* Constant time
* @since 0.9.38
*/
private static boolean isOffline(SigningPrivateKey spk) {
byte b = 0;
byte[] data = spk.getData();
for (int i = 0; i < data.length; i++) {
b |= data[i];
}
return b == 0;
}
/**
* Does this session have offline and transient keys?
* @since 0.9.38
*/
public boolean isOffline() {
return _offlineSignature != null;
}
/**
* Side effect - zeroes out the current signing private key
* @since 0.9.38
*/
public void setOfflineData(long expires, SigningPublicKey transientPub, Signature sig, SigningPrivateKey transientPriv) {
if (!isOffline(signingPrivKey)) {
SigType type = getSigningPrivKey().getType();
byte[] dbytes = new byte[type.getPrivkeyLen()];
signingPrivKey = new SigningPrivateKey(type, dbytes);
}
_offlineExpiration = expires;
_transientSigningPubKey = transientPub;
_offlineSignature = sig;
_transientSigningPrivKey = transientPriv;
}
/**
* @return Java time (ms) or 0 if not initialized or does not have offline keys
* @since 0.9.38
*/
public long getOfflineExpiration() {
return _offlineExpiration;
}
/**
* @since 0.9.38
*/
public Signature getOfflineSignature() {
return _offlineSignature;
}
/**
* @return null on error or if not initialized or does not have offline keys
* @since 0.9.38
*/
public SigningPublicKey getTransientSigningPubKey() {
try {
// call this to force initialization
getDestination();
} catch (Exception e) {
return null;
}
return _transientSigningPubKey;
}
/**
* @return null on error or if not initialized or does not have offline keys
* @since 0.9.38
*/
public SigningPrivateKey getTransientSigningPrivKey() {
try {
// call this to force initialization
getDestination();
} catch (Exception e) {
return null;
}
return _transientSigningPrivKey;
}
//// end offline methods
public I2PSession open() throws I2PSessionException, IOException { public I2PSession open() throws I2PSessionException, IOException {
return this.open(new Properties()); return this.open(new Properties());
} }
@ -546,6 +730,13 @@ public class PrivateKeyFile {
this.dest.writeBytes(out); this.dest.writeBytes(out);
this.privKey.writeBytes(out); this.privKey.writeBytes(out);
this.signingPrivKey.writeBytes(out); this.signingPrivKey.writeBytes(out);
if (isOffline()) {
DataHelper.writeLong(out, 4, _offlineExpiration / 1000);
DataHelper.writeLong(out, 2, _transientSigningPubKey.getType().getCode());
_transientSigningPubKey.writeBytes(out);
_offlineSignature.writeBytes(out);
_transientSigningPrivKey.writeBytes(out);
}
} finally { } finally {
if (out != null) { if (out != null) {
try { out.close(); } catch (IOException ioe) {} try { out.close(); } catch (IOException ioe) {}
@ -582,8 +773,19 @@ public class PrivateKeyFile {
s.append("\nPrivate Key: "); s.append("\nPrivate Key: ");
s.append(this.privKey); s.append(this.privKey);
s.append("\nSigining Private Key: "); s.append("\nSigining Private Key: ");
s.append(this.signingPrivKey); if (isOffline()) {
s.append("\n"); s.append("offline\nOffline Signature Expires: ");
s.append(new Date(getOfflineExpiration()));
s.append("\nTransient Signing Public Key: ");
s.append(_transientSigningPubKey);
s.append("\nOffline Signature: ");
s.append(_offlineSignature);
s.append("\nTransient Signing Private Key: ");
s.append(_transientSigningPrivKey);
} else {
s.append(this.signingPrivKey);
s.append("\n");
}
return s.toString(); return s.toString();
} }

View File

@ -1,3 +1,16 @@
2018-12-04 zzz
* Data: Add preliminary PrivateKeyFile support for LS2 offline keys (proposal #123)
* I2CP: Add preliminary support for LS2 offline keys (proposal #123)
2018-12-03 zzz
* I2CP: Consolidate all the port 7654 definitions
* NetDb: Don't send our RI in response to DSM when shutting down
* Wizard: Update text
2018-12-02 zzz
* Router: Allow LS2 DSM down a tunnel
* Transport: Add methods to force-disconnect a peer
2018-12-01 zzz 2018-12-01 zzz
* I2CP: Add preliminary support for LS2 (proposal #123) * I2CP: Add preliminary support for LS2 (proposal #123)
* Router: More support for LS2 types (proposal #123) * Router: More support for LS2 types (proposal #123)

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */ /** deprecated */
public final static String ID = "Monotone"; public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION; public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 7; public final static long BUILD = 8;
/** for example "-test" */ /** for example "-test" */
public final static String EXTRA = ""; public final static String EXTRA = "";