Search code examples
javaencryptionbase64rsader

Build PublicKey from Base64 DER - Java


I am having an issue in passing a base64 encoded DER cert to a Java application for it to extract the public key from. I can make this work in both objective-c & ruby, but am struggling with an error in Java.

I have the following DER base64 key created in Ruby (simplified):

key = OpenSSL::PKey::RSA.new(2048)

public_key = key.public_key

subject = "/C=BE/O=Test/OU=Test/CN=Test"

cert = OpenSSL::X509::Certificate.new
cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
cert.not_before = Time.now
cert.not_after = Time.now + 365 * 24 * 60 * 60
cert.public_key = public_key
cert.serial = 0x0
cert.version = 2

ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = cert
ef.issuer_certificate = cert
cert.extensions = [
  ef.create_extension("basicConstraints","CA:TRUE", true),
  ef.create_extension("subjectKeyIdentifier", "hash"),
  # ef.create_extension("keyUsage", "cRLSign,keyCertSign", true),
]
cert.add_extension ef.create_extension("authorityKeyIdentifier",
                                       "keyid:always,issuer:always")

cert.sign key, OpenSSL::Digest::SHA1.new


der => Base64.encode64(cert.to_der)

Here is an example output:

MIIDhzCCAm+gAwIBAgIBADANBgkqhkiG9w0BAQUFADA6MQswCQYDVQQGEwJC\nRTENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVz\ndDAeFw0xNTA1MjExNDUxNTZaFw0xNjA1MjAxNDUxNTZaMDoxCzAJBgNVBAYT\nAkJFMQ0wCwYDVQQKDARUZXN0MQ0wCwYDVQQLDARUZXN0MQ0wCwYDVQQDDARU\nZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvbC3phpnj/Vg\neIAmWXD3TkGi91kPFBvrrD/LLa4kv83eOuY139vUn/xjZlM9maE36Yix6Ix1\ncF3cGCUl1VZApJYTef404jL13xLg1i9+96/tU91niZMlRkFL0mZWV2XhEzNH\nnA+lRiJZlGdXNwYUKXb9qVRuS2taSAyMwH/SDPu1s17SGVLY1o+7trAQfK7w\ny5w8fyTr+tCdcplb5F/m6R5FXc4eJwD6m9MYnenlmkoW5uM5B1lbQBVz2by3\nVGCMUmLwpC15pi13/fIJ8WzD1SQcYJgIQTauWVRYxSuc+Cg0VvoyMZNqkqOW\n7iKmZXGqwNQKxhLtcc1KNJky7OHLkQIDAQABo4GXMIGUMA8GA1UdEwEB/wQF\nMAMBAf8wHQYDVR0OBBYEFNWHBjgj9rsOBm6Z9+me3I/E1ZrUMGIGA1UdIwRb\nMFmAFNWHBjgj9rsOBm6Z9+me3I/E1ZrUoT6kPDA6MQswCQYDVQQGEwJCRTEN\nMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVzdIIB\nADANBgkqhkiG9w0BAQUFAAOCAQEAbKYmQxaQaGT57Qq8xYzIzODGqjbek3qC\n8kSjUto9H/5o8OCKqDFJfgaYAS9mgEjjazqmMahoDeLvzRkKHkpXLvdjjjv7\nnnMZGIw7I4yOKvtzGDz2eimonlWPePypTwr0NFnnUByQb9nkrOOrpcSKBn7a\nwvIT7b82ISOoMz1+hlyo8dyiZri82J6pKXTP91LcfpSRiC/1W1sXnIL5DSJi\nDtXGMVtDfy9rRgPJhmOPu4xqInl/o+t2A1OXLhA4aDnxP/gbssVau9Do3uIa\nOlyo9eGpatIvkxCMzC4SgBavBy6Gsk2p4KAuWon9TtDzO5vklEI8QKk1tiyJ\nYZBCeK3HwQ==\n

If I was to get the public key out of this in Ruby, I would do this:

der = Base64.decode64(data["certs"]["device_key"])
x509_cert = OpenSSL::X509::Certificate.new  der
public_rsa_2048_key = x509_cert.public_key

The same in Java is a little more verbose, but (credit to SO) this is what I have:

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.io.File;
import java.security.cert.CertificateFactory;
import java.io.ByteArrayInputStream;
import java.security.cert.Certificate;
import java.security.PublicKey;
import java.io.FileInputStream;
import java.nio.charset.Charset;
import java.nio.channels.FileChannel;
import java.io.ByteArrayOutputStream;
import javax.xml.bind.DatatypeConverter;
import java.nio.channels.Channels;
import java.io.Console;



public class EncryptionTest{

  public static void main(String[] args) throws IOException, GeneralSecurityException {
    Console console = System.console();

    console.writer().println("Loading base64 key from file");

    // get a handle on the base64 encoded key and certificate
    // File privateKeyFile = new File("private.der.b64");
    File publicKeyFile = new File("public.der.b64");

    console.writer().println("Converting to byteArray");

    // pull them into arrays
    // byte[] privateKeyBytes = toByteArray(privateKeyFile);
    byte[] publicKeyBytes = toByteArray(publicKeyFile);

    console.writer().println("decoding base64 bytes");

    // decode them
    // privateKeyBytes = toDecodedBase64ByteArray(privateKeyBytes);
    publicKeyBytes = toDecodedBase64ByteArray(publicKeyBytes);

    // get the private key
    // KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    // KeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
    // PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);

    console.writer().println("Building Cert");

    // get the public key
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
    Certificate certificate = certificateFactory.generateCertificate(new ByteArrayInputStream(publicKeyBytes));

    console.writer().println("Extracting Public Key");  

    PublicKey publicKey = certificate.getPublicKey();

    console.writer().println("Key:" + publicKey.toString());
  }

  private static byte[] toByteArray(File file) throws IOException {
    // java 7's try-with-resources statement
    try (FileInputStream in = new FileInputStream(file);
        FileChannel channel = in.getChannel()) {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      channel.transferTo(0, channel.size(), Channels.newChannel(out));
      return out.toByteArray();
    }
  }

  private static byte[] toDecodedBase64ByteArray(byte[] base64EncodedByteArray) {
    return DatatypeConverter.parseBase64Binary(
        new String(base64EncodedByteArray, Charset.forName("UTF-8")));
  }
}

However when I run this I get the following error:

Exception in thread "main" java.security.cert.CertificateException: Unable to initialize, java.io.IOException: DerInputStream.getLength(): lengthTag=105, too big.
    at sun.security.x509.X509CertImpl.<init>(X509CertImpl.java:199)
    at sun.security.provider.X509Factory.engineGenerateCertificate(X509Factory.java:98)
    at java.security.cert.CertificateFactory.generateCertificate(CertificateFactory.java:339)
    at EncryptionTest.main(EncryptionTest.java:50)
Caused by: java.io.IOException: DerInputStream.getLength(): lengthTag=105, too big.
    at sun.security.util.DerInputStream.getLength(DerInputStream.java:561)
    at sun.security.util.DerValue.<init>(DerValue.java:252)
    at sun.security.util.DerInputStream.getDerValue(DerInputStream.java:417)
    at sun.security.x509.X509CertImpl.parse(X509CertImpl.java:1761)
    at sun.security.x509.X509CertImpl.<init>(X509CertImpl.java:196)

Any pointers on how to solve this in Java would be great! Thanks


Solution

  • You need to replace the \n in your example output by a linefeed character. Then your code will not fail.

    output (long line truncated)

    Loading base64 key from file
    Converting to byteArray
    decoding base64 bytes
    Building Cert
    Extracting Public Key
    Key:Sun RSA public key, 2048 bits
      modulus: 239461822256650353755672 ...
      public exponent: 65537
    

    edit One possible solution to replace the \n could be to to replace it by two linefeed characters (only because of not to change the number of bytes in the array).

    Amend your method toByteArray as below.

    private static byte[] toByteArray(File file) throws IOException {
        byte[] allBytes = Files.readAllBytes(file.toPath());
        for (int i = 0; i < allBytes.length; i++) {
            if (allBytes[i] == '\\') {
                allBytes[i++] = 10;
                allBytes[i++] = 10;
            }
        }
        return allBytes;
    }