Sunday, October 27, 2013

JCE を使って Diffie-Hellman をやってみる

VPN を実現するプロトコルのひとつに IPsec というものがあります. IPsec においては鍵交換プロトコルとして IKE を利用するのが一般的なのではないかと思いますが, その IKE において利用することのできる鍵交換アルゴリズムのひとつに Diffie-Hellman 鍵交換アルゴリズムがあります.

このアルゴリズムは, 大雑把に言うと, 秘密鍵を共有していない当事者間において, 各自が乱数などを使って計算して生成した値を互いに相手に送り合い, 各自が相手から受け取った値をもとに再び計算すると, 結果的に双方が同じ秘密鍵を保持することができる, というものです. 通信経路上に存在する盗聴者が当事者間でやりとりされる値だけから秘密鍵を推測することは容易ではないらしく, そのことがこのアルゴリズムの効能となっています.

Diffie-Hellman 鍵交換アルゴリズムは, JCE (Java Cryptography Extension) でも扱うことができます. ふと試してみたくなりまして, 少し学んでみました. 以下, その際のメモを書き残しておくことにします.

けっこうたくさん嘘を書いているかもしれないので, その際にはご指摘いただけると幸いです.

鍵交換の仕様を決める

Diffie-Hellman 鍵交換アルゴリズムでは, ふたつの自然数 pg の値を決めておき, 当事者が同じものを使うことが必要です. わたしには JCE の全体像がまだよくわかっていなくて, いくつか方法がありそうに思うのですが, とりあえず DHParameterSpec クラスのインスタンスを生成し, そのインスタンスに pg の定義を保持させることにしてみました.

        String modp1024 =
                "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" +
                "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
                "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" +
                "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" +
                "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" +
                "FFFFFFFFFFFFFFFF";
        BigInteger p = new BigInteger(modp1024, 16);
        BigInteger g = BigInteger.valueOf(2);
        DHParameterSpec dhParameterSpec = new DHParameterSpec(p, g);

ここで使うことにした p および g は, IKE において "Group 2" だとか "1024-bit MODP" だとかいう名前で呼ばれて定義されている値です. p は 1024 ビットの素数です. IKE ではほかに 512 ビット, 1536 ビット, 2048 ビット, 3072 ビット, ... などなど, いくつかの素数が定義されています. しかしながら, わたしがいま使っている JDK 6u45 の Sun JCE プロバイダでは, どうやら 512 ビット以上 1024 ビット以下の素数しか扱えないようです. 1536 ビットの素数で試そうとしたら, このあと鍵ペアを作る過程で InvalidAlgorithmParameterException がスローされ, そのメッセージには,

Prime size must be multiple of 64, and can only range from 512 to 1024 (inclusive)

と記されていました.

鍵ペアを作る

各当事者は, 鍵交換の仕様に基づき, 鍵ペアを作ります. たとえば次のようになりましょうか.

        KeyPairGenerator generator;
        KeyPair keyPair;
        try {
            generator = KeyPairGenerator.getInstance("DiffieHellman");
            generator.initialize(dhParameterSpec);
            keyPair = generator.generateKeyPair();
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        }

getInstance メソッドの引数として渡すアルゴリズム名は, "DH" でもよいようです. generateKeyPair メソッドは, 呼ばれるつど新たな鍵ペアを生成するようです.

公開鍵を相手に送る

相手方には, 自分の公開鍵を送ることが必要です. 公開鍵を体現する PublicKey インスタンスでは getEncoded メソッドを使うことができます. 公開鍵をバイト列にエンコードしてくれるようです. これを使ったシリアライズが妥当な選択肢なのかどうか確信が持てないのですが, 最も簡単で取り扱いやすいと感じたので, これを使うことにしました.

        return keyPair.getPublic().getEncoded();

相手の公開鍵を受け取る

相手方からも公開鍵が送られてきましたら, 今度はバイト列にエンコードされたものを PublicKey インスタンスにデコード (デシリアライズ) することになります. 基本的に公開鍵は X.509 形式でエンコードされることになっているようなので, KeyFactory クラスと X509EncodedKeySpec クラスを使えば, デコード (デシリアライズ) することができそうです.

        KeySpec keySpec = new X509EncodedKeySpec(encodedPublicKey);
        KeyFactory factory;
        PublicKey publicKey;
        try {
            factory = KeyFactory.getInstance("DiffieHellman");
            publicKey = factory.generatePublic(keySpec);
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        }

鍵合意をおこないます

自分が保持する私有鍵と相手から受け取った公開鍵から, いよいよ秘密鍵を生成します. 英語では Key Agreement と言いますが, 日本語では何と言えばよいのでしょう. 鍵合意か鍵協定かといったところでしょうか. ニュアンスとしては協定よりも合意のほうが近いかなと思いましたので, とりあえず, 鍵合意と呼ぶことにします.

        KeyAgreement agreement;
        SecretKey secretKey;
        try {
            agreement = KeyAgreement.getInstance("DiffieHellman");
            agreement.init(keyPair.getPrivate(), dhParameterSpec);
            agreement.doPhase(publicKey, true);
            secretKey = agreement.generateSecret("AES");
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        }

私有鍵 (keyPair.getPrivate()) は自分の私有鍵をあらわし, 公開鍵 (publicKey) は相手方の公開鍵をあらわしています. KeyAgreement インスタンスに対して init メソッドと doPhase メソッドを実行することで, 秘密鍵が生成されます. doPhase メソッドの第 2 引数は, もし当事者が 3 名以上いるのであれば doPhase を複数回おこなう必要が出てきますので, 最終フェーズに至らないうちは false とします. 当事者 2 名の場合は, フェーズは 1 回のみでいいはずですので, true です.

上のコードでは, generateSecret メソッドの引数に "AES" を渡しました. この場合, 返って来る SecretKey インスタンスは, AES 用の鍵ということになります. デフォルトでは 256 ビット長の鍵となるようです. ちなみに, 引数なしの generateSecret メソッドを利用すると, 1024 ビット長のバイト列が返って来るのですが, そのうち先頭の 256 ビット分が AES 用の鍵として抽出されているようでした.

おためし

おためし用のコードを書いてみました. まず, 鍵交換の当事者を体現するクラス EndPoint を次のようにしてみました. (各メソッドで引数チェックは省略しています.)

package tt4cs.dh;

import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.spec.KeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import javax.crypto.spec.DHParameterSpec;

public class EndPoint {
    
    private final String name;
    
    private DHParameterSpec dhParameterSpec;
    private KeyPair keyPair;
    private SecretKey secretKey;
    
    public EndPoint(String name) {
        this.name = name;
    }
    
    private void initialize(DHParameterSpec dhParameterSpec) {
        this.dhParameterSpec = dhParameterSpec;
        KeyPairGenerator generator;
        try {
            generator = KeyPairGenerator.getInstance("DiffieHellman");
            generator.initialize(dhParameterSpec);
            keyPair = generator.generateKeyPair();
        } catch (GeneralSecurityException e) {
            throw new RuntimeException("Initialization was failed.", e);
        }
    }
    
    /**
     * 鍵交換の仕様に従い, まず鍵ペアを新規作成します.
     * そのうえで, 公開鍵をバイト列にエンコードして返します.
     */
    public byte[] generatePublicKey(DHParameterSpec dhParameterSpec) {
        initialize(dhParameterSpec);
        return keyPair.getPublic().getEncoded();
    }
    
    /**
     * バイト列にエンコードされた公開鍵を受け取り, 鍵合意をおこないます.
     * そして, 指定された暗号アルゴリズムで利用するための秘密鍵を生成し,
     * それを内部で保持します.
     */
    public void agreeSecretKey(byte[] encodedPublicKey, String algorithm) {
        KeySpec keySpec = new X509EncodedKeySpec(encodedPublicKey);
        KeyFactory factory;
        try {
            factory = KeyFactory.getInstance("DiffieHellman");
            agreeSecretKey(factory.generatePublic(keySpec), algorithm);
        } catch (GeneralSecurityException e) {
            throw new RuntimeException("Conversion was failed.", e);
        }
    }
    
    private void agreeSecretKey(PublicKey publicKey, String algorithm) {
        KeyAgreement agreement;
        try {
            agreement = KeyAgreement.getInstance("DiffieHellman");
            agreement.init(keyPair.getPrivate(), dhParameterSpec);
            agreement.doPhase(publicKey, true);
            secretKey = agreement.generateSecret(algorithm);
        } catch (GeneralSecurityException e) {
            throw new RuntimeException("Agreement was failed.", e);
        }
    }
    
    /**
     * 現在のステータスを標準出力に表示します.
     */
    public String getStatus() {
        if (keyPair == null)
            return String.format("%s: Not initialized yet.", name);
        if (secretKey == null)
            return String.format("%s: Not agreed yet.", name);
        StringBuilder sb = new StringBuilder();
        sb.append(name);
        sb.append(": Agreed.\n");
        byte[] bytes = secretKey.getEncoded();
        for (byte b : bytes)
            sb.append(String.format("%02X", b & 0xFF));
        return sb.toString();
    }
    
}

そのうえで, 実際に次のようなコードを書いて, 観察してみました.

package tt4cs.dh;

import java.math.BigInteger;
import javax.crypto.spec.DHParameterSpec;

public class TestDrive {
    
    public static void main(String... args) {
        
        long t0, t1;
        
        String modp1024 =
                "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" +
                "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
                "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" +
                "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" +
                "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" +
                "FFFFFFFFFFFFFFFF";
/*
        String modp1536 =
                "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" +
                "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
                "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" +
                "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" +
                "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" +
                "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" +
                "83655D23DCA3AD961C62F356208552BB9ED529077096966D" +
                "670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF";
*/
        
        BigInteger p = new BigInteger(modp1024, 16);
        BigInteger g = BigInteger.valueOf(2);
        DHParameterSpec dhParameterSpec = new DHParameterSpec(p, g);
        
        t0 = System.currentTimeMillis();
        
        EndPoint alice = new EndPoint("Alice");
        EndPoint bob = new EndPoint("Bob");
        System.out.println(alice.getStatus());
        System.out.println(bob.getStatus());
        
        byte[] alicePubKey = alice.generatePublicKey(dhParameterSpec);
        byte[] bobPubKey = bob.generatePublicKey(dhParameterSpec);
        System.out.println(alice.getStatus());
        System.out.println(bob.getStatus());
        
        alice.agreeSecretKey(bobPubKey, "AES");
        bob.agreeSecretKey(alicePubKey, "AES");
        System.out.println(alice.getStatus());
        System.out.println(bob.getStatus());
        
        t1 = System.currentTimeMillis();
        System.out.println(String.format("%d msec", t1 - t0));
        
    }
    
}

Alice と Bob が鍵交換する様子をあらわしたつもりです. これの結果は, たとえば次のようになりました.

Alice: Not initialized yet.
Bob: Not initialized yet.
Alice: Not agreed yet.
Bob: Not agreed yet.
Alice: Agreed.
A51AA4B7B44AA01D7ACA81461AF48449CEBF6FDBC76EA6F1B238A98A7C60C857
Bob: Agreed.
A51AA4B7B44AA01D7ACA81461AF48449CEBF6FDBC76EA6F1B238A98A7C60C857
280 msec

Alice と Bob は, それぞれ同一の秘密鍵を生成することができたようです.

まとめ

今回は, そもそも JCE のことも実はほとんどよくわかっていなくて, とりあえずググってみて最初に参考になったのは, JCEでDiffie-Hellman鍵交換を行う - まどぎわBLOG (Feb 2, 2008) という記事でした. ありがとうございます. そこから, 登場してくるクラスの Javadoc を読んだりなんかしているうちに, ここまで来ました. Java だからでしょうか, 全体的にやや冗長な感じがしなくもないのですが. それから, やはり十分に大きい整数 (素数) を扱う処理は, それなりに時間がかかるようですね.

まあ, でも, こういうことを勉強するのは楽しいと感じています. できれば, こういうことが何らかの役に立つような, そういう仕事に就きたいなあと思います. 毎日トラブル対応 (釈明と謝罪と文書作成) ばかりで, 売り上げに寄与する仕事に興味を持つだけで取引先さんの逆鱗に触れるプリセールスエンジニアという, こういう肩書きと実態の乖離した職業には, そろそろ疲れてきました. いや, 本当に疲れました.

No comments:

Post a Comment