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.pem
private2.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:署名だけでも、検証だけでも、公開鍵と秘密鍵の両方が必要になる。