Java ssh-rsa string to public key

14,757

Solution 1

Here is my SSH RSA -> RSAPublicKey converter implementation. I've found key format description somewhere in the net, so thanks to the one who provided it.

public class CertificateUtils {
    private static final int VALUE_LENGTH = 4;
    private static final byte[] INITIAL_PREFIX = new byte[]{0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61};
    private static final Pattern SSH_RSA_PATTERN = Pattern.compile("ssh-rsa[\\s]+([A-Za-z0-9/+]+=*)[\\s]+.*");

// SSH-RSA key format
//
//        00 00 00 07             The length in bytes of the next field
//        73 73 68 2d 72 73 61    The key type (ASCII encoding of "ssh-rsa")
//        00 00 00 03             The length in bytes of the public exponent
//        01 00 01                The public exponent (usually 65537, as here)
//        00 00 01 01             The length in bytes of the modulus (here, 257)
//        00 c3 a3...             The modulus

    public static RSAPublicKey parseSSHPublicKey(String key) throws InvalidKeyException {
        Matcher matcher = SSH_RSA_PATTERN.matcher(key.trim());
        if (!matcher.matches()) {
            throw new InvalidKeyException("Key format is invalid for SSH RSA.");
        }
        String keyStr = matcher.group(1);

        ByteArrayInputStream is = new ByteArrayInputStream(Base64.decodeBase64(keyStr));

        byte[] prefix = new byte[INITIAL_PREFIX.length];

        try {
            if (INITIAL_PREFIX.length != is.read(prefix) || !ArrayUtils.isEquals(INITIAL_PREFIX, prefix)) {
                throw new InvalidKeyException("Initial [ssh-rsa] key prefix missed.");
            }

            BigInteger exponent = getValue(is);
            BigInteger modulus = getValue(is);

            return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent));
        } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
            throw new InvalidKeyException("Failed to read SSH RSA certificate from string", e);
        }
    }

    private static BigInteger getValue(InputStream is) throws IOException {
        byte[] lenBuff = new byte[VALUE_LENGTH];
        if (VALUE_LENGTH != is.read(lenBuff)) {
            throw new InvalidParameterException("Unable to read value length.");
        }

        int len = ByteBuffer.wrap(lenBuff).getInt();
        byte[] valueArray = new byte[len];
        if (len != is.read(valueArray)) {
            throw new InvalidParameterException("Unable to read value.");
        }

        return new BigInteger(valueArray);
    }
}

Hope this helps.

Solution 2

You have to convert your key to pkcs8 spec. Use below command

ssh-keygen -f private.key -e -m pkcs8 > test-pkcs8.pub

Then convert it to x509

openssl rsa -pubin -in test-pkcs8.pub -outform pem > test-x509.pem

You can then use below code to read the public key as RSAPublicKey in Java

import java.io.IOException;

import java.net.URISyntaxException;

import java.nio.file.Files;

import java.nio.file.Paths;

import java.security.KeyFactory;

import java.security.NoSuchAlgorithmException;

import java.security.PrivateKey;

import java.security.interfaces.RSAPublicKey;

import java.security.spec.InvalidKeySpecException;

import java.security.spec.PKCS8EncodedKeySpec;

import java.security.spec.X509EncodedKeySpec;

import java.util.Base64;


/**

* This file is intended to be used on a IDE for testing purposes.

* ClassLoader.getSystemResource won't work in a JAR

*/

public class Main {


    public static void main(String[] args) throws InvalidKeySpecException, NoSuchAlgorithmException, IOException, URISyntaxException {


        String privateKeyContent = new String(Files.readAllBytes(Paths.get(ClassLoader.getSystemResource("private_key_pkcs8.pem").toURI())));

        String publicKeyContent = new String(Files.readAllBytes(Paths.get(ClassLoader.getSystemResource("public_key.pem").toURI())));


        privateKeyContent = privateKeyContent.replaceAll("\\n", "").replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "");

        publicKeyContent = publicKeyContent.replaceAll("\\n", "").replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");;


        KeyFactory kf = KeyFactory.getInstance("RSA");


        PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyContent));

        PrivateKey privKey = kf.generatePrivate(keySpecPKCS8);


        X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyContent));

        RSAPublicKey pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509);


        System.out.println(privKey);

        System.out.println(pubKey);

    }

}

Got the answer from below two links

Converting ssh-rsa to X509 Spec in Java

Loading X509 spec key in Java as RSAPublicKey object

Hope this will give you some intuition.

Solution 3

I found a lot of answers how to get the public key - but none of them actually contained the part how to get the openssh public key as a string - it got a special format.

Cudos to @Jcs and @James K Polk

This depends on BouncyCastle. It could probably be done without.

package cuul.stuff;

import lombok.SneakyThrows;
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Security;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;

/**
 * Takes an private SSH key and cranks out the corresponding public one.
 *
 * Just what this command would have done: <pre>ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub</pre>
 *
 * @link https://stackoverflow.com/questions/3706177/how-to-generate-ssh-compatible-id-rsa-pub-from-java
 * @link https://stackoverflow.com/questions/7216969/getting-rsa-private-key-from-pem-base64-encoded-private-key-file/7221381#7221381
 *
 * Why - because I can.
 */
public class ExtractPublicFromPrivateSshKey {

    private static final String BEGIN_RSA_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n";
    private static final String END_RSA_PRIVATE_KEY = "-----END RSA PRIVATE KEY-----";

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    @SneakyThrows
    public static String extract(String privateKeyString) {
        if (!privateKeyString.startsWith(BEGIN_RSA_PRIVATE_KEY)) {
            throw new InvalidKeySpecException("Can only extract public key from a RSA private. "
                    + "This is not an RSA key (header should have been '" + BEGIN_RSA_PRIVATE_KEY + "'");
        }

        privateKeyString = privateKeyString.replace(BEGIN_RSA_PRIVATE_KEY, "");
        privateKeyString = privateKeyString.replace(END_RSA_PRIVATE_KEY, "");
        privateKeyString = privateKeyString.trim();

        byte[] privateKeyBytes = Base64.getMimeDecoder().decode(privateKeyString);

        BCRSAPrivateCrtKey rsaPrivateKey = (BCRSAPrivateCrtKey) getPrivate(privateKeyBytes);

        //create a KeySpec and let the Factory due the Rest. You could also create the KeyImpl by your own.
        RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(
                new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent()));

        byte[] bytes = encodePublicKey(publicKey);
        return "ssh-rsa " + new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8) + " some@user";
    }

    private static PrivateKey getPrivate(byte[] privateKeyBytes)
            throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(spec);
    }

    /**
     * @link https://stackoverflow.com/questions/3706177/how-to-generate-ssh-compatible-id-rsa-pub-from-java
     *
     * The key format used by ssh is defined in the RFC #4253. The format for RSA public key is the following :

     * string    "ssh-rsa"
     * mpint     e  // key public exponent
     * mpint     n  // key modulus
     *
     * All data type encoding is defined in the section #5 of RFC #4251. string and mpint (multiple precision integer) types are encoded this way :
     *
     * 4-bytes word: data length (unsigned big-endian 32 bits integer)
     * n bytes     : binary representation of the data
     *
     * or instance, the encoding of the string "ssh-rsa" is:
     *
     * byte[] data = new byte[] {0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a'};
     */
    private static byte[] encodePublicKey(RSAPublicKey key) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        /* encode the "ssh-rsa" string */
        byte[] sshrsa = new byte[] {0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a'};
        out.write(sshrsa);
        /* Encode the public exponent */
        BigInteger e = key.getPublicExponent();
        byte[] data = e.toByteArray();
        encodeUInt32(data.length, out);
        out.write(data);
        /* Encode the modulus */
        BigInteger m = key.getModulus();
        data = m.toByteArray();
        encodeUInt32(data.length, out);
        out.write(data);
        return out.toByteArray();
    }

    private static void encodeUInt32(int value, OutputStream out) throws IOException {
        byte[] tmp = new byte[4];
        tmp[0] = (byte)((value >>> 24) & 0xff);
        tmp[1] = (byte)((value >>> 16) & 0xff);
        tmp[2] = (byte)((value >>> 8) & 0xff);
        tmp[3] = (byte)(value & 0xff);
        out.write(tmp);
    }
}

Solution 4

Late response but I had the same issue and came up with the following: You'll need Apache commons-io and guava libraries

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.spec.RSAPublicKeySpec;

import org.apache.commons.io.IOUtils;

import com.google.common.base.Splitter;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import com.google.common.base.Charsets;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterables.get;
import static com.google.common.collect.Iterables.size;
import static com.google.common.io.BaseEncoding.base64;

public class SSHEncodedToRSAPublicConverter {

  private static final String SSH_MARKER = "ssh-rsa";

  private ByteSource supplier;

  public SSHEncodedToRSAPublicConverter(String fileName) {
    this(new File(fileName));
  }

  public SSHEncodedToRSAPublicConverter(File file) {
    try {
      byte[] data = IOUtils.toByteArray(new FileInputStream(file));
      this.supplier = ByteSource.wrap(data);
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }
  }

  public SSHEncodedToRSAPublicConverter(byte[] data) {
    this.supplier = ByteSource.wrap(data);
  }

  /**
   * Converts an SSH public key to a x.509 compliant format RSA public key spec
   * Source: https://github.com/jclouds/jclouds/blob/master/compute/src/main/java/org/jclouds/ssh/SshKeys.java
   * @return RSAPublicKeySpec
   */
  public RSAPublicKeySpec convertToRSAPublicKey() {
    try {
      InputStream stream = supplier.openStream();
      Iterable<String> parts = Splitter.on(' ').split(IOUtils.toString(stream, Charsets.UTF_8));
      checkArgument(size(parts) >= 2 && SSH_MARKER.equals(get(parts,0)), "bad format, should be: ssh-rsa AAAB3....");
      stream = new ByteArrayInputStream(base64().decode(get(parts, 1)));
      String marker = new String(readLengthFirst(stream));
      checkArgument(SSH_MARKER.equals(marker), "looking for marker %s but received %s", SSH_MAKER, marker);
      BigInteger publicExponent = new BigInteger(readLengthFirst(stream));
      BigInteger modulus = new BigInteger(readLengthFirst(stream));
      RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
      return keySpec;
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }
  }

  private static byte[] readLengthFirst(InputStream in) throws IOException {
    int[] bytes = new int[]{ in.read(), in.read(), in.read(), in.read() };
    int length = 0;
    int shift = 24;
    for (int i = 0; i < bytes.length; i++) {
      length += bytes[i] << shift;
      shift -= 8;
    }
    byte[] val = new byte[length];
    ByteStreams.readFully(in, val);
    return val;
  }
}

Then to use it you can do something like:

File keyFile = new File("id_rsa.pub");
Keyspec spec = new SSHEncodedToRSAPublicConverter(keyFile).convertToRSAPublicKey();
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key = kf.generatePublic(spec);

I got the conversion (special thanks) portion from the following link:

https://github.com/jclouds/jclouds/blob/master/compute/src/main/java/org/jclouds/ssh/SshKeys.java

Solution 5

Code for getting PublicKey object from .pub file generated through ssh-keygen.

RSAPublicKeySpec constructor looks like this

public RSAPublicKeySpec(BigInteger modulus, BigInteger publicExponent)

So we need to extract modulus and publicExponent from .pub file and pass it to the constructor to create RSAPublicKeySpec. Once we have the spec we can generate PublicKey using KeyFactory.

private PublicKey decodePublicKey() {
    try {
        // input stream of .pub file
        InputStream inputStream = new ClassPathResource("keys/test_public_key.key").getInputStream(); 
        String keyLine = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
        String[] parts = keyLine.split(" ");
        for (String part : parts) {
            if (part.startsWith("AAAA")) {
                byte[] decodeBuffer = Base64Utils.decode(part.getBytes());
                ByteBuffer bb = ByteBuffer.wrap(decodeBuffer);
                /* using 4 bytes from bb to generate integer which gives us length of key- 
                format type, in this case len=7 as "ssh-rsa" has 7 chars  
                */
                int len = bb.getInt(); 
                byte[] type = new byte[len];
                bb.get(type);
                if ("ssh-rsa".equals(new String(type))) {
                    // extracting exponent and modulus from remaining byte-buffer
                    BigInteger exponent = decodeBigInt(bb);
                    BigInteger modulus = decodeBigInt(bb);
                    RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
                    return KeyFactory.getInstance("RSA").generatePublic(spec);
                } else {
                    throw new IllegalArgumentException("Only supporta RSA");
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
private BigInteger decodeBigInt(ByteBuffer bb) {
    // use first 4 bytes to generate an Integer that gives the length of bytes to create BigInteger
    int len = bb.getInt();
    byte[] bytes = new byte[len];
    bb.get(bytes);
    return new BigInteger(bytes);
}

References :

Share:
14,757
Jan Wytze
Author by

Jan Wytze

Updated on June 19, 2022

Comments

  • Jan Wytze
    Jan Wytze almost 2 years

    I want to get the public key of the content of an .pub file. This is an example what the content of a .pub file looks like(generated with ssh-keygen):

    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDBPL2s+25Ank3zS6iHUoVk0tS63dZM0LzAaniiDon0tdWwq4vcL4+fV8BsAEcpMeijS92JhDDc9FccXlHbdDcmd6c4ITOt9h9xxhIefGsi1FTVJ/EjVtbqF5m0bu7ruIMGvuP1p5s004roHx9y0UdHvD/yNWLISMhy4nio6jLailIj3FS53Emj1WRNsOrpja3LzPXzhuuj6YnD9yfByT7iGZipxkmleaXrknChPClLI9uhcqtAzBLdd0NVTJLOt/3+d1cSNwdBw9e53wJvpEmH+P8UOZd+oV/y7cHIej4jQpBXVvpJR1Yaluh5RuxY90B0hSescUAj4g/3HVPpR/gE7op6i9Ab//0iXF15uWGlGzipI4lA2/wYEtv8swTjmdCTMNcTDw/1huTDEzZjghIKVpskHde/Lj416c7eSByLqsMg2OhlZGChKznpIjhuNRXz93DwqKuIKvJKSnhqaJDxmDGfG7nlQ/eTwGeAZ6VR50yMPiRTIpuYd767+Nsg486z7p0pnKoBlL6ffTbfeolUX2b6Nb9ZIOxJdpCSNTQRKQ50p4Y3S580cUM1Y2EfjlfIQG1JdmTQYB75AZXi/cB2PvScmF0bXRoj7iHg4lCnSUvRprWA0xbwzCW/wjNqw6MyRX42FFlvSRrmfaxGZxKYbmk3TzBv+Fp+CADPqQm3OQ== [email protected]
    

    If I am right this is not the public key, but it is possible to get the public key from this string.

    This answer gives answer to my question https://stackoverflow.com/a/19387517/2735398
    But the answer doesn't seem to work. I get an exception:

    java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format
    

    When looking at the comments of the answer I am not the only person with the problem...

    How can I fix the exception? Or is there another way to get the public key from the string?