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!");