楕円曲線暗号の鍵をJavaで読み込む

OpenSSLで生成した楕円曲線暗号の鍵をJavaで読み込む方法。RSAはあるけど楕円曲線が無かったので。

# 手順

## OpenSSLで鍵生成

```bat
$ 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`

ポイント１：`-name` で指定する楕円曲線の一覧は `openssl ecparam -list_curves` で出力できる。
ポイント２：秘密鍵は**PKCS#8形式に変換する（重要！）**。 

## Javaで読み込み

- 秘密鍵

```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
```

- 公開鍵

```java
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使う](https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt)ので、事前にいれておいてください。

- JWT生成（署名）

```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.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パーズ（検証）

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