マイナンバーカードのJava APIを利用して電子署名する

本記事では、Java APIを利用して、マイナンバーカードに搭載されている公的個人認証サービスの電子証明書(公開鍵、秘密鍵)を利用した電子署名とその検証方法を見ていきます。

【目次】

[1]はじめに

本記事では、Java APIを利用して、マイナンバーカードに搭載されている公的個人認証サービスの電子証明書(公開鍵、秘密鍵)を利用した電子署名とその検証方法を見ていきます。

なお、マイナンバーカードに搭載されている電子証明書の概要や、Java APIから電子証明書にアクセスする方法については、記事『マイナンバーカードの電子証明書(公的個人認証サービス)にJava APIでアクセスする』を参照して下さい。

さて、公的個人認証サービスは、「インターネットを通じて安全・確実な行政手続き等を行うために、他人によるなりすまし申請や電子データが通信途中で改ざんされていないことを確認するための機能」を提供するものとされています。

この「他人によるなりすまし申請や電子データが通信途中で改ざんされていないことを確認」することを実現する技術が電子署名やPKIになりますが、今回はJava APIを利用した電子署名を中心に見ていきます。

ところで、用語として、厳密には「電子署名(electronic signature)」と「デジタル署名(digital signature)」を区別することもあるようですが、本記事では公開鍵暗号方式を用いた署名を電子署名として書くことにします。(JPKIライブラリの仕様書にも電子署名という用語で書かれていますので。)

[2]電子署名と検証の概要

電子署名は、なりすまし、改ざん、否認という脅威を防ぐ技術です。

例えば、AさんがBさんへ安全・確実に電子的なメッセージを送信したいとします。
Aさんは、送信したいメッセージに電子署名を行ってBさんへ送信します。Bさんは電子署名を検証することで、受信したメッセージがなりすましや改ざんされていないか確認することができます。

実際には、下記に書いているような電子署名の検証だけでなく、電子証明書のパス検証や電子証明書の有効性の確認などの処理も必要です。

なお、電子署名やPKIについてはネットに多くの情報がありますので、参考にしてください。
また、本記事で利用するマイナンバーカード(公的個人認証)のJava APIの詳細については、以下の資料を参照して下さい。

前置きが長くなりましたが、これから基本的な電子署名とその検証に絞って見ていきます。

まず、本記事のサンプルコードの元になる電子署名とその検証の基本的な流れをざっくり図にしてみました。


電子署名とその検証の基本的な流れ


図にある処理を説明します。

<Aさん:電子署名の生成>

  1. 一方向ハッシュ関数(SHA256など)を使ってメッセージのダイジェスト(ハッシュ値)を計算します。
  2. ダイジェストをAさんの秘密鍵で暗号化して署名値を生成します。
  3. メッセージと署名値に加えてAさんの証明書(公開鍵を含む)をBさんに送ります。

<Bさん:電子署名の検証>

  1. 受信したメッセージから、Aさんが使用したのと同じ一方向ハッシュ関数を使ってダイジェストを計算します。
  2. 受信したAさんの証明書から、Aさんの公開鍵を取り出します。
  3. 受信した署名値を、Aさんの公開鍵で復号してダイジェストを得ます。
  4. メッセージから計算したダイジェストと、署名値から得られたダイジェストを比較します。
    • 2つのダイジェストが完全に一致したとき、署名の検証は成功です。これにより次のことが確認できます。
      • メッセージが改ざんされていないこと
      • メッセージは、検証に使用した公開鍵と対になる秘密鍵によって署名されたこと
    • 2つのダイジェストが一致しないとき、署名の検証は失敗です。これにより次のどちらかの事象が発生していることが確認できます。(どちらが原因かまでは分かりません。)
      • メッセージが改ざんされたこと
      • 電子署名が改ざんされたこと

(補足)

  • 本来は、署名検証に成功した時、秘密鍵が確かにAさん本人のものであることを確実にするために、PKI(Public Key Infrastracture:公開鍵基盤)が利用されます(証明書パスの検証や証明書の有効性確認などを行います。)
  • 上記の流れは、説明を簡単にするために、メッセージ、証明書、署名値を個別送信するイメージで書いていますが、実際にはPKCS#7(CMS)、XML署名、PDF署名など、電子署名の標準フォーマットでパッケージングして、(利用する暗号アルゴリズムなどの他の情報も一緒に)送信するのが一般的です。
  • 行政手続きのオンライン申請は上記の電子署名とGPKIの枠組みによって意思表示(印鑑の代わり)や認証(身分証明)を実現していると言えます。
  • 民間の取引等でマイナンバーカードによる電子署名を利用しようとすると、改ざんの検出や証明書のパス検証までは行えますが、証明書の有効性確認ができないという問題があります。(『マイナンバーカードの電子証明書(公的個人認証サービス)にJava APIでアクセスする』で書いたように、マイナンバーカードとそのパスワードが無いと証明書の有効性確認ができません。)
  • ただし、平成28年1月より、民間事業者においても署名検証者・利用者証明検証者として、公的個人認証サービスを利活用できるようになったようです。

[3]電子署名(カードAPライブラリを利用)

<Aさん:電子署名の生成>で書いた流れを、Java APIで実装してみます。

ここでは、まず、送信したいメッセージを与えると、SHA256のダイジェストを計算して、電子証明書の秘密鍵で暗号化した署名値を返すサンプルコードを作成します。
そして、メッセージ、証明書、署名値をファイルに保存します。

なお、カードAPライブラリ:Java APIの詳細については、JPKIのサイトで提供されているJavadocを参照して下さい。

(1)署名用電子証明書

以下のコードは、送信したいメッセージを与えると、SHA256のダイジェストを計算して、署名用電子証明書の秘密鍵で暗号化した署名値を返すサンプルコードです。

利用する場合は、JPKICryptSignJNI.jar にクラスパスを通します。
このコードを実行すると「個人番号カード ログイン」画面が表示されますので、ここで署名用パスワードを入力する必要があります。

/**
 * メッセージのダイジェスト(SHA256)を計算し、署名用電子証明書の秘密鍵で署名値を生成
 * @param message
 * @return
 * @throws JPKICryptSignJNIException
 */
public static byte[] signMessage_SignJNI_SHA256withRSA_Sample( byte[] message) throws JPKICryptSignJNIException{
    JPKICryptSignJNI jni = new JPKICryptSignJNI();
    //(1)プロバイダハンドルを取得
    long hProvider = jni.cryptAcquireContext(0);
    try {
        //(2)ハッシュハンドルを生成しハンドルを取得
        long hHash = jni.cryptCreateHash(hProvider, JPKICryptSignJNI.JPKI_CALG_SHA_256);
        try {
            //(3)データをハッシュ
            jni.cryptHashData(hHash, message);
            //(4)ハッシュに署名し署名値を取得
            return jni.cryptSignHash(hHash);
        } finally {
            //(5)ハッシュを解放
            jni.cryptDestroyHash(hHash);
        }
    } finally {
        //(6)プロバイダハンドルを解放
        jni.cryptReleaseContext(hProvider);
    }
}

この関数を用いて、メッセージ(testという文字列データ)、署名値、証明書をファイルへ保存してみます。

// メッセージ例:テキストをバイト配列へ
byte[] message = "test".getBytes();
// 署名値を生成
byte[] sig = signMessage_SignJNI_SHA256withRSA_Sample(message);
// 証明書を取得
byte[] cert = getSignCertificateValue_Sample();

// ファイルへ出力
Files.write(Paths.get("message.bin"), message);
Files.write(Paths.get("sig.bin"), sig);
Files.write(Paths.get("sig_cert.cer"), cert);


なお、このサンプルコードを実行すると、署名値生成と証明書取得で、それぞれ「個人番号カード ログイン」画面が表示されて、2回パスワードを入力する必要がありますが、実際に利用する場合は、パスワード入力を1回とするように処理を設計すべきと思います。

(2)利用者証明用証明書

署名用電子証明書と同様に、利用者証明用証明書を利用した署名も行えます。
署名用電子証明書のサンプルとほぼ同じで、違いは以下の通りです。
  • JPKICryptAuthJNI.jar にクラスパスを通す必要がある
  • JPKICryptAuthJNIを利用する(JPKICryptSignJNIクラスではない)
  • 例外クラスの型がJPKICryptAuthJNIExceptionになる(JPKICryptSignJNIExceptionではない)
実行すると「個人番号カード ログイン」画面が表示されますので、ここで利用者証明用パスワードを入力する必要があります。

/**
 * メッセージのダイジェスト(SHA256)を計算し、署名用電子証明書の秘密鍵で署名値を生成
 * @param message
 * @return
 * @throws JPKICryptAuthJNIException
 */
public static byte[] signMessage_AuthJNI_SHA256withRSA_Sample( byte[] message) throws JPKICryptAuthJNIException{
    JPKICryptAuthJNI jni = new JPKICryptAuthJNI();
    //(1)プロバイダハンドルを取得
    long hProvider = jni.cryptAcquireContext(0);
    try {
        //(2)ハッシュハンドルを生成しハンドルを取得
        long hHash = jni.cryptCreateHash(hProvider, JPKICryptSignJNI.JPKI_CALG_SHA_256);
        try {
            //(3)データをハッシュ
            jni.cryptHashData(hHash, message);
            //(4)ハッシュに署名し署名値を取得
            return jni.cryptSignHash(hHash);
        } finally {
            //(5)ハッシュを解放
            jni.cryptDestroyHash(hHash);
        }
    } finally {
        //(6)プロバイダハンドルを解放
        jni.cryptReleaseContext(hProvider);
    }
}

この関数を用いて、メッセージ(testという文字列データ)、署名値、証明書をファイルへ保存できます。

// メッセージ例:テキストをバイト配列へ
byte[] message = "test".getBytes();
// 署名値を生成
byte[] sig = signMessage_AuthJNI_SHA256withRSA_Sample(message);
// 証明書を取得
byte[] cert = getAuthCertificateValue_Sample();
// ファイルへ出力
Files.write(Paths.get("message.bin"), message);
Files.write(Paths.get("sig.bin"), sig);
Files.write(Paths.get("sig_cert.cer"), cert);


(3)ダイジェストをJavaで計算する場合のサンプル

上記サンプルでは、ダイジェストをカードAPライブラリを利用して計算していますが、以下のように、Javaの標準ライブラリでもダイジェストを計算できます。

/**
 * メッセージに対するSHA-256ダイジェストを計算する
 * @param message
 * @return
 * @throws NoSuchAlgorithmException
 */
public static byte[] digestSHA256(byte[] message) throws NoSuchAlgorithmException {
    return MessageDigest.getInstance("SHA-256").digest(message);
}

このように予めダイジェストを計算している場合、署名値を計算する処理は以下のようになります。

/**
 * ダイジェスト(SHA256)に対して署名用電子証明書の秘密鍵で署名値を生成
 * @param digestSha256
 * @return
 * @throws JPKICryptSignJNIException
 */
public static byte[] signHash_SignJNI_Sample( byte[] digestSha256) throws JPKICryptSignJNIException {
    JPKICryptSignJNI jni = new JPKICryptSignJNI();
    //(1)プロバイダハンドルを取得
    long hProvider = jni.cryptAcquireContext(0);
    try {
        //(2)ハッシュハンドルを生成しハンドルを取得
        long hHash = jni.cryptCreateHash(hProvider, JPKICryptSignJNI.JPKI_CALG_SHA_256);
        try {
             //(3)ハッシュ値(ダイジェスト)をセット
            jni.cryptSetHashValue(hHash, digestSha256);
            //(4)ハッシュに署名し署名値を取得
            return jni.cryptSignHash(hHash);
        } finally {
            //(5)ハッシュを解放
            jni.cryptDestroyHash(hHash);
        }
    } finally {
        //(6)プロバイダハンドルを解放
        jni.cryptReleaseContext(hProvider);
    }
}

以下のコードで、同じ署名値が生成されることを確認できます。

// メッセージ例:テキストをバイト配列へ
byte[] message = "test".getBytes();
// 署名値を生成
byte[] sig1 = signMessage_SignJNI_SHA256withRSA_Sample(message);		
byte[] sig2 = signHash_SignJNI_Sample(digestSHA256(message));
// trueと表示されれば署名値が同じ
System.out.println(Arrays.equals(sig1, sig2));

上記は、署名用電子証明書の例になっていますが、利用者証明用証明書でも同様のことができます。 利用者証明用証明書で署名する場合は、JPKICryptAuthJNI.jar にクラスパスを通し、JPKICryptSignJNIクラスをJPKICryptAuthJNIへ、JPKICryptSignJNIExceptionクラスをJPKICryptAuthJNIExceptionに変更して実行します。

[4]電子署名の検証(Java標準ライブラリを利用)

<Bさん:電子署名の検証>で書いた流れを実装してみます。

電子署名の技術は標準化されていますので、汎用的なライブラリを利用して実装できます。
また、署名用電子証明書か利用者証明証明書に関係なく検証が可能です。

(後述のように、カードAPライブラリにも電子署名の検証機能が提供されていますが、受信者側がカードAPライブラリを準備して検証することは考えにくく、汎用的なライブラリを利用するのが一般的かと思います。)

以下のコードは、Javaの標準ライブラリ(java.security.Signature)を利用した署名の検証のコード例です。

/**
 * JavaのSignatureクラスを利用した署名の検証
 * @param message
 * @param cert
 * @param signature
 * @return
 * @throws NoSuchAlgorithmException
 * @throws InvalidKeyException
 * @throws SignatureException
 * @throws CertificateException
 */
public static boolean verifySignature_Java_Sample(byte[] message, byte[] cert, byte[] signature) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, CertificateException {
    // Javaの証明書オブジェクトに変換
    Certificate x509 = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(cert));
    // 署名の検証
    Signature engine = Signature.getInstance("SHA256withRSA");
    engine.initVerify(x509);
    engine.update(message);
    return engine.verify(signature);        
}

なお、本来はハッシュ関数などもパラメータ化すべきですが、ここではダイジェストがSHA256、RSAの秘密鍵で暗号化されたメッセージであると仮定しています。(署名のサンプルのコードにあわせています。)

以下のコード例で、署名のサンプルで保存したメッセージ、署名値、証明書を復元して署名を検証してみます。

// ファイルからメッセージ、署名値、証明書を復元
byte[] message = Files.readAllBytes(Paths.get("message.bin"));
byte[] sig = Files.readAllBytes(Paths.get("sig.bin"));
byte[] cert = Files.readAllBytes(Paths.get("sig_cert.cer"));

// 署名の検証:trueと表示されれば成功
System.out.println("verifySignatue(Java)=" + verifySignature_Java_Sample(message,cert,sig) );

コンソールにtrueと表示されれば、署名の検証に成功したことになり、
  • メッセージが改ざんされていないこと
  • メッセージは、検証に使用した公開鍵と対になる秘密鍵によって署名されたこと
が確認できます。

また、message.binあるいはsig.binの内容を変更すると、コンソールにfalseと表示されると思います。この場合は、メッセージが改ざんされた、あるいは電子署名が改ざんされたことが確認できます。但し、どちらが改ざんされたかは分かりません。

ちなみに、sig.binの内容はうまく変更しないと例外がスローされます。この場合でも署名の検証に失敗したことには違いありません。

(注意)
本来は署名の検証に加えて、証明書パスや証明書の有効性確認が必要ですが、ここでは割愛しています。(他人の公的個人認証証明書の有効性は(認定事業者を除いて)民間側では確認できません。)

[5]電子署名の検証(カードAPライブラリを利用)

先に書いたように、電子署名の検証は標準ライブラリなど汎用的な暗号ライブラリなどを用いて行うのが一般的だと思いますが、カードAPライブラリを用いて電子署名の検証を行うこともできます。

(参考)
『利用者クライアントソフト 機能概要説明書  第8節 電子署名検証機能』によると、「利用者が行政機関等から受け取った電子公文書等の電子署名を検証する。」と書かれています。
また、カードAPライブラリの機能としては提供されていますが、JPKI利用者ソフトの機能としては提供されていないようです。

以下は、カードAPライブラリを利用した署名の検証を行うサンプルコードです。

/**
 * JPKIカードAPライブラリを利用した署名の検証
 * @param message
 * @param cert
 * @param signature
 * @return
 * @throws JPKICryptSignJNIException
 */
public static boolean verifySignature_JPKI_Sample(byte[] message,byte[] cert,byte[] signature) throws JPKICryptSignJNIException {
    JPKICryptSignJNI jni = new JPKICryptSignJNI();
    //(1)プロバイダハンドルを取得
    long hProvider = jni.cryptAcquireContext(JPKICryptSignJNI.JPKI_VERIFYCONTEXT);
    try {
         //(2)証明書ハンドルを取得
        long hCert = jni.certCreateCertificateContext(cert);
        byte[] pubkey;
        try {
            //(3)証明書の公開鍵を取得
            pubkey = jni.certGetPublicKeyInfo(hCert);
        } finally {
            //(4)証明書ハンドルを解放
            jni.certFreeCertificateContext(hCert);
        }
         //(5)公開鍵への鍵ハンドルを生成しハンドルを取得
        long hKey = jni.cryptImportPublicKey(hProvider, pubkey);
        try {
            //(6)ハッシュハンドルを生成しハンドルを取得
            long hHash = jni.cryptCreateHash(hProvider, JPKICryptSignJNI.JPKI_CALG_SHA_256);
            try {
                //(7)データをハッシュ
                jni.cryptHashData(hHash, message);
                 //(8)署名値を検証
                return jni.cryptVerifySignature(hHash, signature, hKey);
            } finally {
                //(9)ハッシュを解放
                jni.cryptDestroyHash(hHash);
            }
        } finally {
            //(10)公開鍵ハンドルを解放
            jni.cryptDestroyKey(hKey);
        }
    } finally {
        //(11)プロバイダハンドルを解放
        jni.cryptReleaseContext(hProvider);
    }
}


ちなみに上記コード例は、[4]で書いたJava標準ライブラリを利用した署名の検証サンプル(verifySignature_Java_Sample)と同じ引数、戻り値にしていますので、テストコードを置き換えて試すことができます。

参考までに、上記のようなカードAPライブラリを利用して署名検証を行う場合の留意点を書いておきます。
  • 署名の検証を行う際に、マイナンバーカードが認識されている必要があります。
    • カードが認識されていない場合、例外がスローされます。(但し、必ずしも本人のカードである必要はありません。)
    • この点だけ見ても、一般的な署名検証用途に利用するのは難しい。。。
  • certGetPublicKeyInfoで得られる公開鍵の情報について
    • JavaのカードAPライブラリはWindowsのCryptAPIのラッパーです。
    • 上記コードは、APライブラリのcertGetPublicKeyInfoで公開鍵を取り出していますが、これはCryptAPIのCryptExportPublicKeyInfoに対応していると思います。この関数で得られる公開鍵の情報は、CERT_PUBLIC_KEY_INFO 構造体となっています。
    • 同様に、APライブラリのcryptImportPublicKeyで公開鍵からハンドルを作成していますが、これはCryptAPIのCryptImportPublicKeyInfoに対応していると思います。この関数に与える公開鍵の情報は、CERT_PUBLIC_KEY_INFO 構造体となっています。
    • つまり、certGetPublicKeyInfoで得られる公開鍵の情報は、一般的な公開鍵のバイナリ形式ではなく、CryptAPIのCERT_PUBLIC_KEY_INFO構造体の形となっているようです。
    • 一方、Javaの標準ライブラリを利用して証明書から公開鍵情報を取得する場合、Certificate.getPublicKey.getEncodedを利用することが多いと思いますが、これはX.509証明書で定義されているsubjectPublicKeyInfoのバイナリ表現です。
    • どちらも同様の項目を持っていますが、バイナリのレイアウトが異なります。このため、カードAPライブラリを利用する際にJavaのCertificateから得たバイナリを与えるとエラーになりますので注意が必要です。

コメント

このブログの人気の投稿

VirtualBoxのスナップショット機能

Google Document AIで画像から表形式データを抽出する(Vision API OCRとの違い)

Ubuntu/Colab環境でPDFファイルのページを画像化する(pdf2image、pdftoppm、pdftocairo)