diff --git a/core/java/src/net/i2p/client/impl/I2CPMessageProducer.java b/core/java/src/net/i2p/client/impl/I2CPMessageProducer.java index 4f59355eb16cc381a657c957c8cfbf572370b5be..ac8d1e1684b64c2f3af944fb525fe1f1bd6311f0 100644 --- a/core/java/src/net/i2p/client/impl/I2CPMessageProducer.java +++ b/core/java/src/net/i2p/client/impl/I2CPMessageProducer.java @@ -95,17 +95,18 @@ class I2CPMessageProducer { CreateSessionMessage msg = new CreateSessionMessage(); SessionConfig cfg = new SessionConfig(session.getMyDestination()); cfg.setOptions(session.getOptions()); - if (_log.shouldLog(Log.DEBUG)) _log.debug("config created"); + if (session.isOffline()) { + cfg.setOfflineSignature(session.getOfflineExpiration(), + session.getTransientSigningPublicKey(), + session.getOfflineSignature()); + } try { cfg.signSessionConfig(session.getPrivateKey()); } catch (DataFormatException dfe) { throw new I2PSessionException("Unable to sign the session config", dfe); } - if (_log.shouldLog(Log.DEBUG)) _log.debug("config signed"); msg.setSessionConfig(cfg); - if (_log.shouldLog(Log.DEBUG)) _log.debug("config loaded into message"); session.sendMessage_unchecked(msg); - if (_log.shouldLog(Log.DEBUG)) _log.debug("config message sent"); } /** diff --git a/core/java/src/net/i2p/data/i2cp/SessionConfig.java b/core/java/src/net/i2p/data/i2cp/SessionConfig.java index 3e419a6dc0abdf06e7cb4d54e62da05467374cc4..932b2f1cf2764ceab25ce0862ad3e4c1cbfdac90 100644 --- a/core/java/src/net/i2p/data/i2cp/SessionConfig.java +++ b/core/java/src/net/i2p/data/i2cp/SessionConfig.java @@ -19,12 +19,14 @@ import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.crypto.DSAEngine; +import net.i2p.crypto.SigType; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.DataStructureImpl; import net.i2p.data.Destination; import net.i2p.data.Signature; import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; import net.i2p.util.Clock; import net.i2p.util.Log; import net.i2p.util.OrderedProperties; @@ -40,6 +42,24 @@ public class SessionConfig extends DataStructureImpl { private Date _creationDate; private Properties _options; + /** + * Seconds since epoch, NOT ms + * @since 0.9.38 + */ + public static final String PROP_OFFLINE_EXPIRATION = "i2cp.leaseSetOfflineExpiration"; + + /** + * Base 64, optionally preceded by sig type and ':', default DSA-SHA1 + * @since 0.9.38 + */ + public static final String PROP_TRANSIENT_KEY = "i2cp.leaseSetTransientPublicKey"; + + /** + * Base 64, optionally preceded by sig type and ':', default DSA-SHA1 + * @since 0.9.38 + */ + public static final String PROP_OFFLINE_SIGNATURE = "i2cp.leaseSetOfflineSignature"; + /** * If the client authorized this session more than the specified period ago, * refuse it, since it may be a replay attack. @@ -98,6 +118,8 @@ public class SessionConfig extends DataStructureImpl { * Defaults are not serialized out-of-JVM, and the router does not recognize defaults in-JVM. * Client side must promote defaults to the primary map. * + * Does NOT make a copy. + * * @param options Properties for this session */ public void setOptions(Properties options) { @@ -112,10 +134,96 @@ public class SessionConfig extends DataStructureImpl { _signature = sig; } + /** + * Set the offline signing data. + * Does NOT validate the signature. + * Must be called AFTER setOptions(). Will throw ISE otherwise. + * Side effect - modifies options. + * + * @throws IllegalStateException + * @since 0.9.38 + */ + public void setOfflineSignature(long expires, SigningPublicKey transientSPK, Signature offlineSig) { + if (_options == null) + throw new IllegalStateException(); + _options.setProperty(PROP_OFFLINE_EXPIRATION, Long.toString(expires / 1000)); + _options.setProperty(PROP_TRANSIENT_KEY, + transientSPK.getType().getCode() + ":" + transientSPK.toBase64()); + _options.setProperty(PROP_OFFLINE_SIGNATURE, offlineSig.toBase64()); + } + + /** + * 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() { + if (_options == null) + return 0; + String s = _options.getProperty(PROP_OFFLINE_EXPIRATION); + if (s == null) + return 0; + try { + return Long.parseLong(s) * 1000; + } catch (NumberFormatException nfe) { + return 0; + } + } + + /** + * @return null on error or if not initialized or does not have offline keys + * @since 0.9.38 + */ + public SigningPublicKey getTransientSigningPublicKey() { + if (_options == null || _destination == null) + return null; + String s = _options.getProperty(PROP_TRANSIENT_KEY); + if (s == null) + return null; + int colon = s.indexOf(':'); + SigType type; + if (colon > 0) { + String stype = s.substring(0, colon); + type = SigType.parseSigType(stype); + if (type == null) + return null; + s = s.substring(colon + 1); + } else { + type = SigType.DSA_SHA1; + } + SigningPublicKey rv = new SigningPublicKey(type); + try { + rv.fromBase64(s); + return rv; + } catch (DataFormatException dfe) { + return null; + } + } + + /** + * @return null on error or if not initialized or does not have offline keys + * @since 0.9.38 + */ + public Signature getOfflineSignature() { + if (_options == null || _destination == null) + return null; + String s = _options.getProperty(PROP_OFFLINE_SIGNATURE); + if (s == null) + return null; + Signature rv = new Signature(_destination.getSigningPublicKey().getType()); + try { + rv.fromBase64(s); + return rv; + } catch (DataFormatException dfe) { + return null; + } + } + /** * Sign the structure using the supplied private key * - * @param signingKey SigningPrivateKey to sign with + * @param signingKey SigningPrivateKey to sign with. + * If offline data is set, must be with the transient key. * @throws DataFormatException */ public void signSessionConfig(SigningPrivateKey signingKey) throws DataFormatException { @@ -134,6 +242,8 @@ public class SessionConfig extends DataStructureImpl { * Note that this also returns false if the creation date is too far in the * past or future. See tooOld() and getCreationDate(). * + * As of 0.9.38, validates the offline signature if included. + * * @return true only if the signature matches */ public boolean verifySignature() { @@ -159,8 +269,35 @@ public class SessionConfig extends DataStructureImpl { return false; } - boolean ok = DSAEngine.getInstance().verifySignature(getSignature(), data, - getDestination().getSigningPublicKey()); + SigningPublicKey spk = getTransientSigningPublicKey(); + if (spk != null) { + // validate offline sig + long expires = getOfflineExpiration(); + if (expires < _creationDate.getTime()) + return false; + Signature sig = getOfflineSignature(); + if (sig == null) + return false; + ByteArrayOutputStream baos = new ByteArrayOutputStream(128); + try { + DataHelper.writeLong(baos, 4, expires / 1000); + DataHelper.writeLong(baos, 2, spk.getType().getCode()); + spk.writeBytes(baos); + } catch (IOException ioe) { + return false; + } catch (DataFormatException dfe) { + return false; + } + byte[] odata = baos.toByteArray(); + boolean ok = DSAEngine.getInstance().verifySignature(sig, odata, 0, odata.length, + _destination.getSigningPublicKey()); + if (!ok) + return false; + } else { + spk = getDestination().getSigningPublicKey(); + } + + boolean ok = DSAEngine.getInstance().verifySignature(getSignature(), data, spk); if (!ok) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(SessionConfig.class); if (log.shouldLog(Log.WARN)) log.warn("DSA signature failed!");