Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reading a PKCS#1 or SPKI public key in Java without libraries

I need to use a public key to verify some data in Java, but I can't seem to format the key in such a way that Java can use without third-party plugins.

I'm generating the key with Node.js's crypto library, which gives me the option of PKCS#1 or SPKI, and either .pem or .der file format.

I've heard that Java doesn't support PKCS#1 out-of-the box, and pretty much every other answer on StackOverflow recommends using BouncyCastle or similar, but in my case, I am writing an SDK, and simply cannot afford to use a library just to read this public key.

So I'm currently reading the key in .der format as it saves having to strip the PEM headers and decode the key from base-64. When I run this, I get the error:

java.security.spec.InvalidKeySpecException: java.lang.RuntimeException: error:0c0000be:ASN.1 encoding routines:OPENSSL_internal:WRONG_TAG

Here's what I have (sorry, it's in Kotlin, not Java like the title suggests)

// Here's a key for convenience
val key = Base64.getDecoder().decode("MFUCTgF/uLsPBS13Gy7C3dPpiDF6SYCLUyyl6CFqPtZT1h5bwKR9EDFLQjG/kMiwkRMcmEeaLKe5qdj9W/FfFitwRAm/8F53pQw2UETKQI2b2wIDAQAB");

val keySpec = X509EncodedKeySpec(key)
val keyFactory = KeyFactory.getInstance("RSA")
val publicKey = keyFactory.generatePublic(keySpec) // error thrown here

val cipher = Cipher.getInstance("RSA/NONE/PKCS1Padding")
cipher.init(Cipher.DECRYPT_MODE, publicKey)

My best idea at the minute is to install a library on the Node.js side, which is less problematic, to support exporting the key as PKCS#8, but I thought I'd check first to see if I'm missing anything.

like image 584
Chris Watts Avatar asked Jan 17 '19 14:01

Chris Watts


1 Answers

The following code turns a PKCS#1 encoded public key into a SubjectPublicKeyInfo encoded public key, which is the public key encoding accepted by the RSA KeyFactory using X509EncodedKeySpec - as SubjectPublicKeyInfo is defined in the X.509 specifications.

Basically it is a low level DER encoding scheme which

  1. wraps the PKCS#1 encoded key into a bit string (tag 0x03, and a encoding for the number of unused bits, a byte valued 0x00);
  2. adds the RSA algorithm identifier sequence (the RSA OID + a null parameter) in front - pre-encoded as byte array constant;
  3. and finally puts both of those into a sequence (tag 0x30).

No libraries are used. Actually, for createSubjectPublicKeyInfoEncoding, no import statements are even required.


import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class PKCS1ToSubjectPublicKeyInfo {

    private static final int SEQUENCE_TAG = 0x30;
    private static final int BIT_STRING_TAG = 0x03;
    private static final byte[] NO_UNUSED_BITS = new byte[] { 0x00 };
    private static final byte[] RSA_ALGORITHM_IDENTIFIER_SEQUENCE =
            {(byte) 0x30, (byte) 0x0d,
                    (byte) 0x06, (byte) 0x09, (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01, (byte) 0x01,
                    (byte) 0x05, (byte) 0x00};


    public static RSAPublicKey decodePKCS1PublicKey(byte[] pkcs1PublicKeyEncoding)
            throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        byte[] subjectPublicKeyInfo2 = createSubjectPublicKeyInfoEncoding(pkcs1PublicKeyEncoding);
        KeyFactory rsaKeyFactory = KeyFactory.getInstance("RSA");
        RSAPublicKey generatePublic = (RSAPublicKey) rsaKeyFactory.generatePublic(new X509EncodedKeySpec(subjectPublicKeyInfo2));
        return generatePublic;
    }

    public static byte[] createSubjectPublicKeyInfoEncoding(byte[] pkcs1PublicKeyEncoding)
    {
        byte[] subjectPublicKeyBitString = createDEREncoding(BIT_STRING_TAG, concat(NO_UNUSED_BITS, pkcs1PublicKeyEncoding));
        byte[] subjectPublicKeyInfoValue = concat(RSA_ALGORITHM_IDENTIFIER_SEQUENCE, subjectPublicKeyBitString);
        byte[] subjectPublicKeyInfoSequence = createDEREncoding(SEQUENCE_TAG, subjectPublicKeyInfoValue);

        return subjectPublicKeyInfoSequence;
    }

    private static byte[] concat(byte[] ... bas)
    {
        int len = 0;
        for (int i = 0; i < bas.length; i++)
        {
            len += bas[i].length;
        }

        byte[] buf = new byte[len];
        int off = 0;
        for (int i = 0; i < bas.length; i++)
        {
            System.arraycopy(bas[i], 0, buf, off, bas[i].length);
            off += bas[i].length;
        }

        return buf;
    }

    private static byte[] createDEREncoding(int tag, byte[] value)
    {
        if (tag < 0 || tag >= 0xFF)
        {
            throw new IllegalArgumentException("Currently only single byte tags supported");
        }

        byte[] lengthEncoding = createDERLengthEncoding(value.length);

        int size = 1 + lengthEncoding.length + value.length;
        byte[] derEncodingBuf = new byte[size];

        int off = 0;
        derEncodingBuf[off++] = (byte) tag;
        System.arraycopy(lengthEncoding, 0, derEncodingBuf, off, lengthEncoding.length);
        off += lengthEncoding.length;
        System.arraycopy(value, 0, derEncodingBuf, off, value.length);

        return derEncodingBuf;
    }   

    private static byte[] createDERLengthEncoding(int size)
    {
        if (size <= 0x7F)
        {
            // single byte length encoding
            return new byte[] { (byte) size };
        }
        else if (size <= 0xFF)
        {
            // double byte length encoding
            return new byte[] { (byte) 0x81, (byte) size };
        }
        else if (size <= 0xFFFF)
        {
            // triple byte length encoding
            return new byte[] { (byte) 0x82, (byte) (size >> Byte.SIZE), (byte) size };
        }

        throw new IllegalArgumentException("size too large, only up to 64KiB length encoding supported: " + size);
    }

    public static void main(String[] args) throws Exception
    {
        // some weird 617 bit key, which is way too small and not a multiple of 8
        byte[] pkcs1PublicKeyEncoding = Base64.getDecoder().decode("MFUCTgF/uLsPBS13Gy7C3dPpiDF6SYCLUyyl6CFqPtZT1h5bwKR9EDFLQjG/kMiwkRMcmEeaLKe5qdj9W/FfFitwRAm/8F53pQw2UETKQI2b2wIDAQAB");
        RSAPublicKey generatePublic = decodePKCS1PublicKey(pkcs1PublicKeyEncoding);
        System.out.println(generatePublic);
    }
}

Notes:

  • NoSuchAlgorithmException should probably be caught and put into a RuntimeException;
  • the private method createDERLengthEncoding should probably not accept negative sizes.
  • Larger keys have not been tested, please validate createDERLengthEncoding for those - I presume it works, but better be safe than sorry.
like image 185
Maarten Bodewes Avatar answered Sep 21 '22 12:09

Maarten Bodewes