OpenSSLで生成した楕円曲線暗号の鍵をJavaで読み込む方法。RSAはあるけど楕円曲線が無かったので。
手順
OpenSSLで鍵生成
$ openssl ecparam -genkey -name secp256r1 -out keypair.pem
$ openssl ec -in keypair.pem -outform PEM -pubout -out public.pem
$ openssl ec -in keypair.pem -outform PEM -out private2.pem
$ openssl pkcs8 -topk8 -nocrypt -in private2.pem -out private.pem
できたもの
- 
public.pem:公開鍵 - 
private.pem:秘密鍵(PKCS#8形式) 
いらないもの
keypair.pemprivate2.pem
ポイント1:-name で指定する楕円曲線の一覧は openssl ecparam -list_curves で出力できる。
ポイント2:秘密鍵はPKCS#8形式に変換する(重要!)。
Javaで読み込み
- 秘密鍵
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class Main {
    public static void main(String[] args) throws Exception {
        var kf = KeyFactory.getInstance("EC");
        var privatePem = readPem("private.pem"); // ①
        var privateDer = Base64.getDecoder().decode(privatePem); // ②
        
        var privateKeySpec = new PKCS8EncodedKeySpec(privateDer);
        var privatekey = (ECPrivateKey) kf.generatePrivate(privateKeySpec); // ➂
        System.out.println(privatekey.getParams());
    }
    private static String readPem(String filename) throws IOException {
        return Files.readString(Paths.get(filename))
                 .replaceAll("-----.+?-----", "")
                 .replaceAll("\\r?\\n", "")
                 .trim();
    }
}
ポイント①:いつものように、PEMのヘッダー、フッター、改行を取る。
ポイント②:Base64デコードして、DER(バイナリ)形式に変換する。
ポイント➂:秘密鍵の読み込み。PKCS#8に変換しないと、次のようなエラーがでる。
Exception in thread "main" java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
	at jdk.crypto.ec/sun.security.ec.ECKeyFactory.engineGeneratePrivate(ECKeyFactory.java:170)
	at java.base/java.security.KeyFactory.generatePrivate(KeyFactory.java:389)
	at test5.Main.main(Main.java:22)
Caused by: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
	at java.base/sun.security.pkcs.PKCS8Key.decode(PKCS8Key.java:135)
	at java.base/sun.security.pkcs.PKCS8Key.<init>(PKCS8Key.java:95)
	at jdk.crypto.ec/sun.security.ec.ECPrivateKeyImpl.<init>(ECPrivateKeyImpl.java:75)
	at jdk.crypto.ec/sun.security.ec.ECKeyFactory.implGeneratePrivate(ECKeyFactory.java:245)
	at jdk.crypto.ec/sun.security.ec.ECKeyFactory.engineGeneratePrivate(ECKeyFactory.java:166)
	... 2 more
- 公開鍵
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.interfaces.ECPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class Main {
    public static void main(String[] args) throws Exception {
        var kf = KeyFactory.getInstance("EC");
        var publicPem = readPem("public.pem"); // ①
        var publicDer = Base64.getDecoder().decode(publicPem); // ②
        
        var publicKeySpec = new X509EncodedKeySpec(publicDer);
        var publicKey = (ECPublicKey) kf.generatePublic(publicKeySpec);
        System.out.println(publicKey);
    }
    private static String readPem(String filename) throws IOException {
        return Files.readString(Paths.get(filename))
                 .replaceAll("-----.+?-----", "")
                 .replaceAll("\\r?\\n", "")
                 .trim();
    }
}
ポイント①:いつものように、PEMのヘッダー、フッター、改行を取る。
ポイント②:Base64デコードして、DER(バイナリ)形式に変換する。
おまけ - JWTの署名と検証
Nimbus使うので、事前にいれておいてください。
- JWT生成(署名)
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.KeyUse;
public class Main {
    public static void main(String[] args) throws Exception {
        var kf = KeyFactory.getInstance("EC");
        var publicPem = readPem("public.pem");
        var publicDer = Base64.getDecoder().decode(publicPem);
        
        var publicKeySpec = new X509EncodedKeySpec(publicDer);
        var publicKey = (ECPublicKey) kf.generatePublic(publicKeySpec);
        System.out.println(publicKey);
        
        var jwk = new ECKey(Curve.P_256, publicKey, privatekey, KeyUse.SIGNATURE, null, null, null, null, null, null, null, null); // ①
        var signer = new ECDSASigner(jwk);
        var jwt = new JWSObject(
                new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(jwk.getKeyID()).build(),
                new Payload("{...}"));
        jwt.sign(signer);
        var str = jwt.serialize();
        System.out.println(str);
    }
    
    private static String readPem(String filename) throws IOException {
        return Files.readString(Paths.get(filename))
                .replaceAll("-----.+?-----", "")
                .replaceAll("\\r?\\n", "")
                .trim();
    }
}
- JWTパーズ(検証)
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.KeyUse;
public class Main {
    public static void main(String[] args) throws Exception {
        var kf = KeyFactory.getInstance("EC");
        var privatePem = readPem("private2.pem");
        var privateDer = Base64.getDecoder().decode(privatePem);
        
        var privateKeySpec = new PKCS8EncodedKeySpec(privateDer);
        var privatekey = (ECPrivateKey) kf.generatePrivate(privateKeySpec);
        System.out.println(privatekey.getParams());
        
        var jwk = new ECKey(Curve.P_256, publicKey, privatekey, KeyUse.SIGNATURE, null, null, null, null, null, null, null, null); // ①
        
        var str = "...(JWTの文字列)...";
        var jwt = JWSObject.parse(str);
        var verifier = new ECDSAVerifier(jwk);
        jwt.verify(verifier);
        System.out.println(jwt);      
    }
    
    private static String readPem(String filename) throws IOException {
        return Files.readString(Paths.get(filename))
                .replaceAll("-----.+?-----", "")
                .replaceAll("\\r?\\n", "")
                .trim();
    }
}
ポイント①-1:楕円曲線の種類は、OpenSSLで生成した曲線名に合わせる。この記事では secp256r1 (P-256) なので、P_256 を指定。
ポイント①-2:署名だけでも、検証だけでも、公開鍵と秘密鍵の両方が必要になる。