Search code examples
javajwtcryptographyjava-securitypkcs#8

.InvalidKeyException: IOException : version mismatch while generating a Private key using a PEM file


I have a .pem file and I am trying to generate a PrivateKey out of it so I can create a JWT (JSON Web Token) that is signed by this private key. However, the code breaks when the KeyFactory.generatePrivate(keySpec) is executed. Below is the contents of the .pem file where the key is present and the java file

mysecret1.pem file

-----BEGIN PRIVATE KEY----- MIGkAgEBBDAz2He6GHzqEe+a2MAl/3QyuvnCsGQrFYKmP/F9mX7lhjYBqN/wJBQd ppqjhVPFiF+gBwYFK4EEACKhZANiAAQvEqdP8J+IkWrx2WA9EEblRiZPaRY9drWI aNFGI24rMBbx5SPd+OkLEOPx1Z5QOJlhdkKtwGsNZRklXGBwFwy/sYV+bRt/bt5O 6SfitF4XZvewKafZ8YaxPklhdloWe+I= -----END PRIVATE KEY-----

JwtSigner1.java:

public class JwtSigner1 {
    
    private static final long EXPIRY_DURATION = 1000L * 60 * 60 * 1; // 1 hour
    
    public static void main(String[] args) throws Exception {
        File file = new File("mysecret1.pem");
        System.out.println(JwtSigner1.generateTokenUsingPemFile(file));
    }
    
    public static String generateTokenUsingPemFile(File file) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        String key = new String(Files.readAllBytes(file.toPath()),       Charset.defaultCharset());
    
        String privateKeyPEM = key.replace("-----BEGIN PRIVATE KEY-----", "").replaceAll(System.lineSeparator(), "")
                .replace("-----END PRIVATE KEY-----", "");
    
        byte[] encoded = Base64.getDecoder().decode(privateKeyPEM);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
    
        Map<String, Object> claims = new HashMap<>();
    
        claims.put("data", " app");
        claims.put("namespace", "sample");
    
        return Jwts.builder().setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + EXPIRY_DURATION))
                .setIssuedAt(new Date(System.currentTimeMillis())).signWith(privateKey).compact();
    }

}

Exception:

Exception in thread "main" java.security.spec.InvalidKeySpecException: 
java.security.InvalidKeyException: IOException : version mismatch: (supported:     00, parsed:     01   
at sun.security.rsa.RSAKeyFactory.engineGeneratePrivate(RSAKeyFactory.java:217)     
at java.security.KeyFactory.generatePrivate(KeyFactory.java:372)    
at c.s.v.d.w.c.util.JwtSigner1.generateTokenUsingPemFile(JwtSigner1.java:41)    
at c.s.v.d.w.c.util.JwtSigner1.main(JwtSigner1.java:28) 
Caused by: java.security.InvalidKeyException: IOException : version mismatch: (supported:     00, parsed:     01    
at sun.security.pkcs.PKCS8Key.decode(PKCS8Key.java:351)     
at sun.security.pkcs.PKCS8Key.decode(PKCS8Key.java:356)     
at sun.security.rsa.RSAPrivateCrtKeyImpl.<init>(RSAPrivateCrtKeyImpl.java:91)   
at sun.security.rsa.RSAPrivateCrtKeyImpl.newKey(RSAPrivateCrtKeyImpl.java:75)   
at sun.security.rsa.RSAKeyFactory.generatePrivate(RSAKeyFactory.java:316)   
at sun.security.rsa.RSAKeyFactory.engineGeneratePrivate(RSAKeyFactory.java:213)     ... 3 more 

I can't use the OpenSSL command line, because I have to programmatically use this pem file and directly convert it to a PrivateKey using Java.

I expect a Private Key to be created and using that a JWT token is generated, which is signed by this key.


Solution

  • That's not a PKCS8-format key. The only correct format of the body in a PEM file labelled BEGIN/END PRIVATE KEY is the PKCS8 unencrypted structure PrivateKeyInfo as defined in RFC5208 section 5 or possibly (but very rarely) its OneAsymmetricKey enhancement in RFC5958 section 2 as specified in RFC7468 section 10, so this key file is in violation of standards. And since the only correct format for Java PKCS8EncodedKeySpec is also the PKCS8 unencrypted format (in binary i.e. not PEM) this file is unusable in Java crypto.

    And it's not an RSA key. It's an (X9-style) EC private key in the SEC1=RFC5915 structure so even if converted to PKCS8 it can't be handled by a KeyFactory for RSA.

    However, since X9EC private scalars have length depending only on the curve-group order, and public points have length depending only on the underlying field order and the point format (compressed or not), the SEC1 structure has length depending only on the curve, which options are selected, and if the public-value option is selected which point format is used. As a result the corresponding PKCS8 structure in DER can consist of a prefix that depends only on the curve, SEC1 options, and conditionally point format, plus the SEC1 structure that you have. And you can do that in Java:

            String key = //new String(Files.readAllBytes(file.toPath()), Charset.defaultCharset());
            // hardcoded for test/demo
            "-----BEGIN PRIVATE KEY-----\n"+ // should be EC PRIVATE KEY instead for SEC1
            "MIGkAgEBBDAz2He6GHzqEe+a2MAl/3QyuvnCsGQrFYKmP/F9mX7lhjYBqN/wJBQd\n"+
            "ppqjhVPFiF+gBwYFK4EEACKhZANiAAQvEqdP8J+IkWrx2WA9EEblRiZPaRY9drWI\n"+
            "aNFGI24rMBbx5SPd+OkLEOPx1Z5QOJlhdkKtwGsNZRklXGBwFwy/sYV+bRt/bt5O\n"+
            "6SfitF4XZvewKafZ8YaxPklhdloWe+I=\n"+
            "-----END PRIVATE KEY-----\n"; // ditto
    
            String privateKeyB64 = key.replaceAll("-----(BEGIN|END) PRIVATE KEY-----", "")
                .replaceAll("\r?\n", ""); // more robust to be lax about CR
            byte[] sec1 = Base64.getDecoder().decode(privateKeyB64);
     
            // correct only for secp256k1 with SEC1 [0] and [1] present and point uncompressed
            // and thus sec1.length = 167
            byte[] prefix = Base64.getDecoder().decode("MIG/AgEAMBAGByqGSM49AgEGBSuBBAAiBIGn");
            byte[] encoded = new byte[prefix.length+167];
            System.arraycopy(prefix,0, encoded,0, prefix.length);
            System.arraycopy(sec1,0, encoded,prefix.length, 167);
    
            PrivateKey privateKey = KeyFactory.getInstance("EC")
                .generatePrivate(new PKCS8EncodedKeySpec(encoded));
    
            // use privateKey -- dummy for demo
            System.out.println(privateKey);