You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2015/06/08 15:50:26 UTC

mina-sshd git commit: [SSHD-487] Add support for writing public keys in OpenSSH format

Repository: mina-sshd
Updated Branches:
  refs/heads/master 9b98f342d -> 6380cc8d8


[SSHD-487] Add support for writing public keys in OpenSSH format


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/6380cc8d
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/6380cc8d
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/6380cc8d

Branch: refs/heads/master
Commit: 6380cc8d860ebc5686b161ed238889fe1c13bd49
Parents: 9b98f34
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Mon Jun 8 16:50:16 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Mon Jun 8 16:50:16 2015 +0300

----------------------------------------------------------------------
 .../org/apache/sshd/common/cipher/ECCurves.java | 61 +++++++++++++++---
 .../keys/AbstractPublicKeyEntryDecoder.java     | 39 +++++++++++-
 .../config/keys/DSSPublicKeyEntryDecoder.java   | 18 ++++++
 .../config/keys/ECDSAPublicKeyEntryDecoder.java | 67 ++++++++++++++++++++
 .../sshd/common/config/keys/KeyUtils.java       |  4 +-
 .../sshd/common/config/keys/PublicKeyEntry.java | 51 +++++++++++++++
 .../config/keys/PublicKeyEntryDecoder.java      | 11 ++++
 .../common/config/keys/RSAPublicKeyDecoder.java | 13 ++++
 .../server/config/keys/AuthorizedKeyEntry.java  | 33 ++++++++++
 .../config/keys/AuthorizedKeyEntryTest.java     | 41 +++++++++++-
 10 files changed, 325 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java b/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java
index 2c1cecc..0c0c66f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java
@@ -19,6 +19,7 @@
 package org.apache.sshd.common.cipher;
 
 import java.math.BigInteger;
+import java.security.spec.ECField;
 import java.security.spec.ECFieldFp;
 import java.security.spec.ECParameterSpec;
 import java.security.spec.ECPoint;
@@ -30,6 +31,7 @@ import java.util.TreeMap;
 import org.apache.sshd.common.digest.BuiltinDigests;
 import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 
 /**
  * Utilities for working with elliptic curves.
@@ -45,12 +47,6 @@ public class ECCurves {
 
     public static final String NISTP521 = "nistp521";
 
-    private static final Map<Integer, String> CURVE_SIZES = new TreeMap<Integer, String>();
-    static {
-        CURVE_SIZES.put(Integer.valueOf(256), NISTP256);
-        CURVE_SIZES.put(Integer.valueOf(384), NISTP384);
-        CURVE_SIZES.put(Integer.valueOf(521), NISTP521);
-    }
 
     public static String getCurveName(ECParameterSpec params) {
         int fieldSize = getCurveSize(params);
@@ -61,12 +57,57 @@ public class ECCurves {
         return curveName;
     }
 
+    /**
+     * Key=num. of bits, value=curve name
+     */
+    private static final Map<Integer, String> SIZE2CURVENAME = 
+            Collections.unmodifiableMap(new TreeMap<Integer, String>() {
+                private static final long serialVersionUID = 1L;    // we're not serializing it
+        
+                {
+                    put(Integer.valueOf(256), NISTP256);
+                    put(Integer.valueOf(384), NISTP384);
+                    put(Integer.valueOf(521), NISTP521);
+                }
+            });
+
+    /**
+     * @param fieldSize The key size in bits
+     * @return The name of the curve - {@code null/empty} if no match found
+     */
     public static String getCurveName(int fieldSize) {
-        return CURVE_SIZES.get(Integer.valueOf(fieldSize));
+        return SIZE2CURVENAME.get(Integer.valueOf(fieldSize));
+    }
+
+    private static final Map<String, Integer> CURVENAME2OCTECTCOUNT = 
+            Collections.unmodifiableMap(new TreeMap<String, Integer>(String.CASE_INSENSITIVE_ORDER) {
+                private static final long serialVersionUID = 1L;    // we're not serializing it
+        
+                {
+                    put(NISTP256, Integer.valueOf(32));
+                    put(NISTP384, Integer.valueOf(48));
+                    put(NISTP521, Integer.valueOf(66));
+                }
+            });
+
+    /**
+     * @param curveName Curve name - case <U>insensitive</U> - ignored
+     * if {@code null}/empty
+     * @return The number of octets used to represent the point(s) for
+     * the curve - {@code null} if no match found
+     */
+    public static Integer getNumPointOctets(String curveName) {
+        if (GenericUtils.isEmpty(curveName)) {
+            return null;
+        } else {
+            return CURVENAME2OCTECTCOUNT.get(curveName);
+        }
     }
 
     public static int getCurveSize(ECParameterSpec params) {
-        return params.getCurve().getField().getFieldSize();
+        EllipticCurve   curve = ValidateUtils.checkNotNull(params, "No EC params", GenericUtils.EMPTY_OBJECT_ARRAY).getCurve();
+        ECField field = ValidateUtils.checkNotNull(curve, "No EC curve", GenericUtils.EMPTY_OBJECT_ARRAY).getField();
+        return ValidateUtils.checkNotNull(field, "No EC field", GenericUtils.EMPTY_OBJECT_ARRAY).getFieldSize();
     }
 
     public static Digest getDigestForParams(ECParameterSpec params) {
@@ -75,8 +116,10 @@ public class ECCurves {
             return BuiltinDigests.sha256.create();
         } else if (size <= 384) {
             return BuiltinDigests.sha384.create();
-        } else {
+        } else if (size <= 521) {
             return BuiltinDigests.sha512.create();
+        } else {
+            throw new UnsupportedOperationException("Unsupported curve size for digest: " + size);
         }
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java
index fc97f13..763a8ce 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AbstractPublicKeyEntryDecoder.java
@@ -22,6 +22,7 @@ package org.apache.sshd.common.config.keys;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.StreamCorruptedException;
 import java.math.BigInteger;
 import java.nio.charset.Charset;
@@ -114,6 +115,43 @@ public abstract class AbstractPublicKeyEntryDecoder<K extends PublicKey> impleme
         return getKeyType().getSimpleName() + ": " + getSupportedTypeNames();
     }
 
+    public static final int encodeString(OutputStream s, String v) throws IOException {
+        return encodeString(s, v, StandardCharsets.UTF_8);
+    }
+
+    public static final int encodeString(OutputStream s, String v, String charset) throws IOException {
+        return encodeString(s, v, Charset.forName(charset));
+    }
+
+    public static final int encodeString(OutputStream s, String v, Charset cs) throws IOException {
+        return writeRLEBytes(s, v.getBytes(cs));
+    }
+
+    public static final int encodeBigInt(OutputStream s, BigInteger v) throws IOException {
+        return writeRLEBytes(s, v.toByteArray());
+    }
+
+    public static final int writeRLEBytes(OutputStream s, byte ... bytes) throws IOException {
+        return writeRLEBytes(s, bytes, 0, bytes.length);
+    }
+
+    public static final int writeRLEBytes(OutputStream s, byte[] bytes, int off, int len) throws IOException {
+        byte[]  lenBytes=encodeInt(s, len);
+        s.write(bytes, off, len);
+        return lenBytes.length + len;
+    }
+
+    public static final byte[] encodeInt(OutputStream s, int v) throws IOException {
+        byte[]  bytes={
+                (byte) ((v >> 24) & 0xFF),
+                (byte) ((v >> 16) & 0xFF),
+                (byte) ((v >>  8) & 0xFF),
+                (byte) (    v     & 0xFF)
+              };
+        s.write(bytes);
+        return bytes;
+    }
+
     public static final String decodeString(InputStream s) throws IOException {
         return decodeString(s, StandardCharsets.UTF_8);
     }
@@ -146,5 +184,4 @@ public abstract class AbstractPublicKeyEntryDecoder<K extends PublicKey> impleme
              | ((bytes[2] & 0xFF) << 8)
              | (bytes[3] & 0xFF);
     }
-
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java
index 62acb4c..05af8ba 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/DSSPublicKeyEntryDecoder.java
@@ -21,16 +21,20 @@ package org.apache.sshd.common.config.keys;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.KeyFactory;
+import java.security.interfaces.DSAParams;
 import java.security.interfaces.DSAPublicKey;
 import java.security.spec.DSAPublicKeySpec;
 import java.security.spec.InvalidKeySpecException;
 import java.util.Collections;
 
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
@@ -57,6 +61,20 @@ public class DSSPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<DSAP
     }
 
     @Override
+    public String encodePublicKey(OutputStream s, DSAPublicKey key) throws IOException {
+        ValidateUtils.checkNotNull(key, "No public key provided", GenericUtils.EMPTY_OBJECT_ARRAY);
+
+        DSAParams keyParams = ValidateUtils.checkNotNull(key.getParams(), "No DSA params available", GenericUtils.EMPTY_OBJECT_ARRAY);
+        encodeString(s, KeyPairProvider.SSH_DSS);
+        encodeBigInt(s, keyParams.getP());
+        encodeBigInt(s, keyParams.getQ());
+        encodeBigInt(s, keyParams.getG());
+        encodeBigInt(s, key.getY());
+        
+        return KeyPairProvider.SSH_DSS;
+    }
+
+    @Override
     public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException {
         return SecurityUtils.getKeyFactory("DSA");
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java
index 29be249..3325a26 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/ECDSAPublicKeyEntryDecoder.java
@@ -19,8 +19,11 @@
 
 package org.apache.sshd.common.config.keys;
 
+import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StreamCorruptedException;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.KeyFactory;
@@ -39,6 +42,7 @@ import org.apache.sshd.common.cipher.ECCurves;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.BufferUtils;
 
 /**
@@ -92,6 +96,20 @@ public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<EC
     }
 
     @Override
+    public String encodePublicKey(OutputStream s, ECPublicKey key) throws IOException {
+        ValidateUtils.checkNotNull(key, "No public key provided", GenericUtils.EMPTY_OBJECT_ARRAY);
+        
+        ECParameterSpec params = ValidateUtils.checkNotNull(key.getParams(), "No EC parameters available", GenericUtils.EMPTY_OBJECT_ARRAY);
+        String curveName = ValidateUtils.checkNotNullAndNotEmpty(ECCurves.getCurveName(params), "Cannot determine curve name", GenericUtils.EMPTY_OBJECT_ARRAY);
+        String keyType = ECCurves.ECDSA_SHA2_PREFIX + curveName;
+        encodeString(s, keyType);
+        // see rfc5656 section 3.1
+        encodeString(s, curveName);
+        ECPointCompression.UNCOMPRESSED.writeECPoint(s, curveName, key.getW());
+        return keyType;
+    }
+
+    @Override
     public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException {
         if (SecurityUtils.hasEcc()) {
             return SecurityUtils.getKeyFactory("EC");
@@ -186,6 +204,21 @@ public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<EC
                     BigInteger  y=octetStringToInteger(yp);
                     return new ECPoint(x, y);
                 }
+                
+                @Override
+                public void writeECPoint(OutputStream s, String curveName, ECPoint p) throws IOException {
+                    Integer elems = ECCurves.getNumPointOctets(curveName);
+                    if (elems == null) {
+                        throw new StreamCorruptedException("writeECPoint(" + name() + ")[" + curveName + "] cannot determine octets count");
+                    }
+                    
+                    int numElements = elems.intValue();
+                    AbstractPublicKeyEntryDecoder.encodeInt(s, 1 /* the indicator */ + 2 * numElements);
+                    s.write(getIndicatorValue());
+                    writeCoordinate(s, "X", p.getAffineX(), numElements);
+                    writeCoordinate(s, "Y", p.getAffineY(), numElements);
+                }
+
             };
 
         private final byte  indicatorValue;
@@ -199,6 +232,40 @@ public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<EC
 
         public abstract ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len);
 
+        public void writeECPoint(OutputStream s, String curveName, ECPoint p) throws IOException {
+            if (s == null) {
+                throw new EOFException("No output stream");
+            }
+
+            throw new StreamCorruptedException("writeECPoint(" + name() + ")[" + p + "] N/A");
+        }
+
+        protected void writeCoordinate(OutputStream s, String n, BigInteger v, int numElements) throws IOException {
+            byte[]  vp=v.toByteArray();
+            int     startIndex=0;
+            int     vLen=vp.length;
+            if (vLen > numElements) {
+                if (vp[0] == 0) {   // skip artificial positive sign
+                    startIndex++;
+                    vLen--;
+                }
+            }
+
+            if (vLen > numElements) {
+                throw new StreamCorruptedException("writeCoordinate(" + name() + ")[" + n + "]"
+                                                 + " value length (" + vLen + ") exceeds max. (" + numElements + ")"
+                                                 + " for " + v);
+            }
+
+            if (vLen < numElements) {
+                byte[]  tmp=new byte[numElements];
+                System.arraycopy(vp, startIndex, tmp, numElements - vLen, vLen);
+                vp = tmp;
+            }
+
+            s.write(vp, startIndex, vLen);
+        }
+
         public static final Set<ECPointCompression> VALUES=
                 Collections.unmodifiableSet(EnumSet.allOf(ECPointCompression.class));
         public static final ECPointCompression fromIndicatorValue(int value) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
index 1f1de20..c4f808d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
@@ -125,14 +125,14 @@ public class KeyUtils {
         
         synchronized(byKeyTypeDecodersMap) {
             {
-                PublicKeyEntryDecoder<? extends PublicKey>  decoder=byKeyTypeDecodersMap.get(keyType);
+                PublicKeyEntryDecoder<? extends PublicKey>  decoder=byKeyClassDecodersMap.get(keyType);
                 if (decoder != null) {
                     return decoder;
                 }
             }
             
             // in case it is a derived class
-            for (PublicKeyEntryDecoder<? extends PublicKey> decoder : byKeyTypeDecodersMap.values()) {
+            for (PublicKeyEntryDecoder<? extends PublicKey> decoder : byKeyClassDecodersMap.values()) {
                 Class<?> t = decoder.getKeyType();
                 if (t.isAssignableFrom(keyType)) {
                     return decoder;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
index a3dbf8b..28708fb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
@@ -19,9 +19,11 @@
 
 package org.apache.sshd.common.config.keys;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.Serializable;
+import java.io.StreamCorruptedException;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.PublicKey;
@@ -95,6 +97,19 @@ public class PublicKeyEntry implements Serializable {
         return key;
     }
 
+    /**
+     * @param sb The {@link Appendable} instance to encode the data into
+     * @return The {@link PublicKey}
+     * @throws IOException If failed to decode/encode the key
+     * @throws GeneralSecurityException If failed to generate the key
+     * @see #resolvePublicKey()
+     */
+    public PublicKey appendPublicKey(Appendable sb) throws IOException, GeneralSecurityException {
+        PublicKey key = resolvePublicKey();
+        appendPublicKeyEntry(sb, key);
+        return key;
+    }
+
     @Override
     public int hashCode() {
         return Objects.hashCode(getKeyType())
@@ -192,6 +207,42 @@ public class PublicKeyEntry implements Serializable {
     }
 
     /**
+     * @param key The {@link PublicKey}
+     * @return The {@code OpenSSH} encoded data
+     * @throws IllegalArgumentException
+     */
+    public static String toString(PublicKey key) throws IllegalArgumentException {
+        try {
+            return appendPublicKeyEntry(new StringBuilder(Byte.MAX_VALUE), key).toString();
+        } catch(IOException e) {
+            throw new IllegalArgumentException("Failed (" + e.getClass().getSimpleName() + ") to encode: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Encodes a public key data the same way as the {@link #parsePublicKeyEntry(String)} expects it
+     * @param sb The {@link Appendable} instance to encode the data into
+     * @param key The {@link PublicKey}
+     * @return The updated appendable instance
+     * @throws IOException If failed to append the data
+     */
+    public static final <A extends Appendable> A appendPublicKeyEntry(A sb, PublicKey key) throws IOException {
+        @SuppressWarnings("unchecked")
+        PublicKeyEntryDecoder<PublicKey> decoder = (PublicKeyEntryDecoder<PublicKey>) KeyUtils.getPublicKeyEntryDecoder(key);
+        if (decoder == null) {
+            throw new StreamCorruptedException("Cannot retrived decoder for key=" + key.getAlgorithm());
+        }
+        
+        try(ByteArrayOutputStream s=new ByteArrayOutputStream(Byte.MAX_VALUE)) {
+            String keyType = decoder.encodePublicKey(s, key);
+            byte[] bytes = s.toByteArray();
+            String b64Data = Base64.encodeToString(bytes);
+            sb.append(keyType).append(' ').append(b64Data);
+        }
+        
+        return sb;
+    }
+    /**
      * Character used to denote a comment line in the keys file
      */
     public static final char COMMENT_CHAR='#';

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java
index 1ac3ab3..fc43bb3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java
@@ -21,6 +21,7 @@ package org.apache.sshd.common.config.keys;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.security.GeneralSecurityException;
 import java.security.PublicKey;
 import java.util.Collection;
@@ -53,4 +54,14 @@ public interface PublicKeyEntryDecoder<K extends PublicKey> {
     K decodePublicKey(byte ... keyData) throws IOException, GeneralSecurityException;
     K decodePublicKey(byte[] keyData, int offset, int length) throws IOException, GeneralSecurityException;
     K decodePublicKey(InputStream keyData) throws IOException, GeneralSecurityException;
+    
+    /**
+     * Encodes the {@link PublicKey} using the {@code OpenSSH} format - same
+     * one used by the {@code decodePublicKey} method(s)
+     * @param s The {@link OutputStream} to write the data to
+     * @param key The {@link PublicKey} - may not be {@code null}
+     * @return The key type value - one of the {@link #getSupportedTypeNames()}
+     * @throws IOException If failed to generate the encoding
+     */
+    String encodePublicKey(OutputStream s, K key) throws IOException;
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java
index 08d5bc7..f1b5dcb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/RSAPublicKeyDecoder.java
@@ -21,6 +21,7 @@ package org.apache.sshd.common.config.keys;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.KeyFactory;
@@ -30,7 +31,9 @@ import java.security.spec.RSAPublicKeySpec;
 import java.util.Collections;
 
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
@@ -55,6 +58,16 @@ public class RSAPublicKeyDecoder extends AbstractPublicKeyEntryDecoder<RSAPublic
     }
     
     @Override
+    public String encodePublicKey(OutputStream s, RSAPublicKey key) throws IOException {
+        ValidateUtils.checkNotNull(key, "No public key provided", GenericUtils.EMPTY_OBJECT_ARRAY);
+        encodeString(s, KeyPairProvider.SSH_RSA);
+        encodeBigInt(s, key.getPublicExponent());
+        encodeBigInt(s, key.getModulus());
+        
+        return KeyPairProvider.SSH_RSA;
+    }
+
+    @Override
     public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException {
         return SecurityUtils.getKeyFactory("RSA");
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
index 6421dc0..e817405 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
@@ -87,6 +87,39 @@ public class AuthorizedKeyEntry extends PublicKeyEntry {
     }
 
     @Override
+    public PublicKey appendPublicKey(Appendable sb) throws IOException, GeneralSecurityException {
+        Map<String,String> options=getLoginOptions();
+        if (!GenericUtils.isEmpty(options)) {
+            int index = 0;
+            for (Map.Entry<String,String> oe : options.entrySet()) {
+                String key = oe.getKey(), value = oe.getValue();
+                if (index > 0) {
+                    sb.append(',');
+                }
+                sb.append(key);
+                // TODO figure out a way to remember which options where quoted
+                // TODO figure out a way to remember which options had no value
+                if (!Boolean.TRUE.toString().equals(value)) {
+                    sb.append('=').append(value);
+                }
+                index++;
+            }
+
+            if (index > 0) {
+                sb.append(' ');
+            }
+        }
+
+        PublicKey key = super.appendPublicKey(sb);
+        String kc = getComment();
+        if (!GenericUtils.isEmpty(kc)) {
+            sb.append(' ').append(kc);
+        }
+        
+        return key;
+    }
+
+    @Override
     public String toString() {
         String   entry = super.toString();
         String   kc = getComment();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/6380cc8d/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
index 1fd66c9..328a257 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
@@ -19,14 +19,19 @@
 
 package org.apache.sshd.server.config.keys;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.InputStreamReader;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.security.PublicKey;
 import java.util.Collection;
 
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.IoUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -53,7 +58,41 @@ public class AuthorizedKeyEntryTest extends BaseTestSupport {
         
         runAuthorizedKeysTests(AuthorizedKeyEntry.readAuthorizedKeys(url));
     }
-    
+
+    @Test
+    public void testEncodePublicKeyEntry() throws Exception {
+        URL url = getClass().getResource(AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME);
+        assertNotNull("Missing " + AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME + " resource", url);
+        
+        StringBuilder sb = new StringBuilder(Byte.MAX_VALUE); 
+        try(BufferedReader rdr = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
+            for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
+                line = GenericUtils.trimToEmpty(line);
+                if (GenericUtils.isEmpty(line) || (line.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) {
+                    continue;
+                }
+                
+                int pos = line.indexOf(' ');
+                String keyType = line.substring(0, pos), data = line;
+                // assume this happens if starts with login options
+                if (KeyUtils.getPublicKeyEntryDecoder(keyType) == null) {
+                    data = line.substring(pos + 1).trim();
+                }
+                
+                AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(data);
+                if (sb.length() > 0) {
+                    sb.setLength(0);
+                }
+
+                PublicKey key = entry.appendPublicKey(sb);
+                assertNotNull("No key for line=" + line, key);
+                
+                String encoded = sb.toString();
+                assertEquals("Mismatched encoded form for line=" + line, data, encoded);
+            }
+        }
+    }
+
     @Test
     @Ignore("It might cause some exceptions if user's file contains unsupported keys")
     public void testReadDefaultAuthorizedKeysFile() throws Exception {