LoginSignup
36

More than 3 years have passed since last update.

JCA (Java 暗号化アーキテクチャ)使い方メモ

Last updated at Posted at 2019-04-30

JCA とは

Java Cryptography Architecture(Java 暗号化アーキテクチャ)の略。
Java で暗号技術を使うための API、フレームワーク。

環境

Java

openjdk 11.0.2

OS

Windows 10 (64bit)

前提知識

JCA を使うには、暗号技術についての基礎知識(どういう技術があるのか、どういう仕組なのかとか)が必要になる。
これを知っておかないと、クラス構成の意味や正しい使い方が理解できない恐れがある。
最悪、実際の利用シチュエーションでは問題のある使い方をしてしまい、セキュリティホールを埋め込んでしまうかもしれない。

なので、まずはそもそもの暗号技術について勉強しておく必要がある。

暗号技術についての説明は こちら を参照。

事前定義している関数

検証には jshell を利用している。
よく利用する処理はあらかじめ関数として定義しておいて、特に説明することなく利用している。

事前定義している関数
import java.nio.*
import java.nio.file.*
import java.io.*

// バイト配列を 16 進数表記の文字列に変換する
String toHexString(byte[] bytes) {
    ByteBuffer buffer = ByteBuffer.wrap(bytes);
    StringBuilder sb = new StringBuilder();
    for (int i=0; i<bytes.length; i+=4) {
        sb.append(String.format("%08x", buffer.getInt()));
    }
    return sb.toString();
}

// バイト配列をファイルに出力する
void writeFile(String path, byte[] bytes) throws IOException {
    Files.write(Paths.get(path), bytes);
}

// ファイルをバイト配列として読み取る
byte[] readFile(String path) throws IOException {
    return Files.readAllBytes(Paths.get(path));
}

注意事項

動作検証は JShell で行っているため、記述を簡略化するために入出力ストリームの close() は省略しています。
実際に利用する場合は、 close() するのをお忘れなきよう。

JCA の基本的な仕組み

設計思想

JCA は次のことを実現することを基本方針として設計されている。

  1. 実装の独立性と相互操作性
  2. アルゴリズムの独立性と拡張性

実装の独立性

JCA が提供する様々な暗号技術(暗号化・ハッシュ関数・デジタル署名・etc...)には、様々な事情(米国の法律や新しい暗号技術の登場などなど)により複数の実装から構成されている。
この、暗号技術の実装を提供するコンポーネントのことを暗号化サービス・プロバイダ(もしくは単にプロバイダ)と呼ぶ。

実装の独立性とは、これらの実装を提供するプロバイダの存在を意識しなくて済むようにすることを指している。

つまり、「~~暗号を使いたいから、それをサポートしている・・・プロバイダを指定する」みたいなことをすることなく、「~~暗号を使いたい」とだけ宣言すれば、あとは JCA がよしなにサポートしているプロバイダを見つけて実装クラスを解決してくれるようになっている。

実装の相互操作性

実装の相互操作性とは、複数のプロバイダを組み合わせられることを指している。

つまり、ハッシュ関数の実装には~~プロバイダを使い、乱数生成には・・・プロバイダを使い、鍵生成には***プロバイダ、デジタル署名と暗号化には@@@プロバイダを使う、みたいなことができるようになっている。

アルゴリズムの独立性

普通、1つの暗号技術には複数のアルゴリズムが存在している。
例えば、ハッシュ関数(暗号技術)であれば SHA-1, SHA-2, SHA-3, MD5 など複数のアルゴリズムが存在する。

アルゴリズムの独立性とは、具体的なアルゴリズムに依存しないように暗号技術を使えることを指している。

例えばハッシュ関数については MessageDigest というクラスで抽象化されている。
このクラスには特定のアルゴリズムに依存しない API が定義されている。
MessageDigest のインスタンスを生成するときにアルゴリズムを指定する必要があるが、その後のハッシュ化の処理はアルゴリズムに関係なく同じ実装で実現できるようになっている。

アルゴリズムの拡張性

アルゴリズムの拡張性とは、新しい暗号技術のアルゴリズムが発明され実装されたときに、それを簡単に追加して使えるようにすることを指している。

暗号化サービス・プロバイダ

1つ以上の暗号技術の実装を提供するコンポーネントを暗号化サービス・プロバイダと呼ぶ。
JCA の文脈で単に「プロバイダ」と言った場合、それは暗号化サービス・プロバイダのことを指している。

JDK には最低1つのプロバイダがインストールされている。
プロバイダは静的にも動的に追加できるようになっている。

実際に JDK にデフォルトでインストールされているプロバイダについては、以下に説明がある(Oracle の Java の資料だから、 OpenJDK だと違う可能性がある?)。
JDKプロバイダ・ドキュメント

プロバイダの検索

特定の暗号技術に関するクラスのインスタンスを生成する場合、アルゴリズムを指定してインスタンスを生成する。

例えば MessageDigest のインスタンスを SHA-256 のアルゴリズムを指定して生成する場合は次のように実装する。

jshell
jshell> import java.security.*

jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

すると、 JCA は裏でインストール済みのプロバイダに対して1つずつ「SHA-256 の MessageDigest をサポートしているか?」と問い合わせる。
そして、最初に見つかったサポートしているプロバイダから具体的な実装を取得するようになっている。

これにより、アプリケーションは具体的なプロバイダを意識することなく暗号技術を利用できるようになる。
一応プロバイダの名前を指定してインスタンスを取得することも可能だが、推奨はされていない。

実行環境にインストールされているプロバイダを確認する

プロバイダを表すクラスとして java.security.Provider というクラスが用意されている。

現在の実行環境にインストールされている全ての Provider は、 java.security.SecuritygetProviders() で取得できる。

jshell
jshell> import java.util.stream.*

jshell> Stream.of(Security.getProviders()).forEach(System.out::println)
SUN version 11
SunRsaSign version 11
SunEC version 11
SunJSSE version 11
SunJCE version 11
SunJGSS version 11
SunSASL version 11
XMLDSig version 11
SunPCSC version 11
JdkLDAP version 11
JdkSASL version 11
SunMSCAPI version 11
SunPKCS11 version 11

ドキュメントとして参照したい場合は 4 JDKプロバイダ・ドキュメント あたりを見ればいいと思う。

エンジン・クラス

特定の暗号技術を提供するクラスのことを、エンジン・クラスと呼ぶ。

具体的なエンジン・クラスには、次のようなものがある(一部のみ)。

クラス 提供する機能
SecureRandom 暗号用に予測不可能性を備えた乱数生成
MessageDigest 暗号用のハッシュ関数
Signature デジタル署名の作成と検証
Cipher 暗号化と復号
Mac メッセージ認証コード
KeyGenerator 秘密鍵の生成
KeyPairGenerator 鍵ペアの生成
KeyStore 鍵を管理するキーストア

インスタンスの取得

jshell
jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

エンジン・クラスには getInstance() という static なファクトリメソッドが用意されている。
このファクトリメソッドの引数にアルゴリズムの名前を指定することで、そのアルゴリズムを実装したインスタンスを取得できる。

なお、アルゴリズムの名前は大文字小文字を区別しない

仕様上サポートされているアルゴリズム

各エンジン・クラスには、仕様上サポートしなければならないアルゴリズムが存在している。

どのアルゴリズムがサポート必須となっているかは、各エンジン・クラスの Javadoc に記載されている。

たとえば MessageDigest クラスであれば Java 11 の時点で次の3つが必須となっている。

  • MD5
  • SHA-1
  • SHA-256

MessageDigest (Java SE 11 & JDK 11 )

実行環境のプロバイダがサポートしているアルゴリズムを確認する

jshell
jshell> var provider = Security.getProviders()[0]
provider ==> SUN version 11

jshell> var services = provider.getServices()
services ==> [SUN: SecureRandom.DRBG -> sun.security.provider. ... ImplementedIn=Software}
]

jshell> services.stream().map(s -> s.getType() + ": " + s.getAlgorithm()).forEach(System.out::println)
SecureRandom: DRBG
SecureRandom: SHA1PRNG
Signature: SHA1withDSA
Signature: NONEwithDSA
Signature: SHA224withDSA
Signature: SHA256withDSA
Signature: SHA1withDSAinP1363Format
Signature: NONEwithDSAinP1363Format
Signature: SHA224withDSAinP1363Format
Signature: SHA256withDSAinP1363Format
KeyPairGenerator: DSA
MessageDigest: MD2
MessageDigest: MD5
MessageDigest: SHA
MessageDigest: SHA-224
MessageDigest: SHA-256
MessageDigest: SHA-384
MessageDigest: SHA-512
MessageDigest: SHA-512/224
MessageDigest: SHA-512/256
MessageDigest: SHA3-224
MessageDigest: SHA3-256
MessageDigest: SHA3-384
MessageDigest: SHA3-512
AlgorithmParameterGenerator: DSA
AlgorithmParameters: DSA
KeyFactory: DSA
CertificateFactory: X.509
KeyStore: PKCS12
KeyStore: JKS
KeyStore: CaseExactJKS
KeyStore: DKS
Policy: JavaPolicy
Configuration: JavaLoginConfig
CertPathBuilder: PKIX
CertPathValidator: PKIX
CertStore: Collection
CertStore: com.sun.security.IndexedCollection

プロバイダが提供する個々の暗号技術機能を表すクラスとして、 java.security.Provider.Service というクラスが用意されている。
この Service は、 Provider に用意されている各種 Getter メソッドから取得できる。

Service にはサポートする暗号技術に関する情報を取得するメソッドが用意されている。
例えば、getType() からは "MessageDigest" のようにそのサービスがサポートする暗号技術の種類を取得でき、
getAlgorithm() メソッドからは、 "SHA-256" のように具体的なアルゴリズムの名前を取得できる。

ドキュメントとして参照したい場合は、 Javaセキュリティ標準アルゴリズム名 を見ればいいと思う。

具体的なプロバイダごとにサポートしているアルゴリズムを確認したい場合は、 JDKプロバイダ・ドキュメント を見ればいいと思う。

アルゴリズムに依存しない API

エンジン・クラスは、そのクラスが提供する暗号技術に関する API を、アルゴリズムに依存しない形で提供している。

つまり、 SHA-256 でも MD5 でも、 MessageDigest を利用すれば同じ実装でハッシュ値を生成できるようになっている。

jshell
// MD5 でハッシュ値を計算
jshell> var md5 = MessageDigest.getInstance("MD5")
md5 ==> MD5 Message Digest from SUN, <initialized>

jshell> md5.update("hoge".getBytes())

jshell> md5.digest()
$7 ==> byte[16] { -22, 112, 62, 122, -95, -17, -38, 0, 100, -22, -91, 7, -39, -24, -85, 126 }

// SHA-256 でハッシュ値を計算
jshell> var sha256 = MessageDigest.getInstance("SHA-256")
sha256 ==> SHA-256 Message Digest from SUN, <initialized>

jshell> sha256.update("hoge".getBytes())

jshell> sha256.digest()
$10 ==> byte[32] { -20, -74, 102, -41, 120, 114, 94, -55, 115, 7, 4, 77, 100, 43, -12, -47, 96, -86, -69, 118, -11, 108, 0, 105, -57, 30, -94, 91, 30, -110, 104, 37 }

↑は、 jshell で MD5 と SHA-256 でハッシュ値を生成している例になる。
MessageDigest のインスタンスを生成するときに指定しているアルゴリズムが異なるだけで、ハッシュ値を計算している部分の実装はどちらのアルゴリズムも同じ形になっている。

パッケージが2つに別れている理由

JCA が提供するクラスは、大きく javax.crypto パッケージと java.security パッケージに分かれて提供されている。

これには歴史的な理由がある。
アメリカはかつて、暗号の輸出を厳しく制限していた時期があった。

アメリカ合衆国からの暗号の輸出規制 - Wikipedia

JCA のパッケージ構成はこの規制に対応したものとなっている。
java.security パッケージには輸出可能な技術に関するクラス(MessageDigestSignature)が入れられ、 javax.crypto パッケージには輸出できない技術に関するクラス(CipherKeyAgreement)が入れられている。

プロバイダもこれらに合わせて分けられていたようで、 SUN プロバイダjava.security で提供している機能を、 SunJCE プロバイダjavax.crypto で提供している機能をそれぞれ実装しているっぽい。

規制が厳しかった頃は、 SunJCE は拡張機能として提供されていたらしいが、現在は規制が緩和され JDK にバンドルされるようになっている。

参考:Javaの暗号化 | 1. 一般的なセキュリティ | セキュリティ開発者ガイド

メッセージダイジェスト(ハッシュ)

ハッシュ関数を使うためには java.security.MessageDigest クラスを使用する。

jshell
jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

jshell> byte[] hash = md.digest("hello world".getBytes())
hash ==> byte[32] { -71, 77, 39, -71, -109, 77, 62, 8, -91 ...  -84, -30, -17, -51, -23 }

digest() メソッドにハッシュ化したい値の byte 配列を渡すと、ハッシュ値が byte 配列で返る。

入力を分割する

jshell
jshell> md.update("hello".getBytes())

jshell> md.update(" world".getBytes())

jshell> var hash = md.digest()
hash ==> byte[32] { -71, 77, 39, -71, -109, 77, 62, 8, -91 ...  -84, -30, -17, -51, -23 }

update() メソッドを使うと、入力を複数に分割できる。

もし一度に全ての byte を入力しなければならないとすると、一旦すべてのデータを byte 配列にしなければならないということになる。
ハッシュ対象のデータサイズが小さい場合は問題ないが、大容量のファイルなどをハッシュ化したい場合は全てのデータをメモリ上に展開することになるため厳しくなる。
そういう場合は、入力データをちょっとずつ update() に渡すことで全てのデータを一度にメモリ上に読み込まなくても済むようになる。

digest() を実行すると MessageDigest の状態は初期化されるので、インスタンスは再利用できる。

16進数の文字列に変換する

digest() の結果は byte 配列なので、そのままだと分かりづらい。
よくハッシュ値の文字列表現として 16進数の文字列が利用されるので、その変換をしてみる。

jshell
// ※toHashString() は、事前定義している関数(ページトップを参照)
jshell> toHexString(hash)
$6 ==> "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"

ファイルのハッシュ値を計算する

ファイルからハッシュ値を計算する場合、自力で FileInputStream からデータを取り出して MessageDigest に入力する方法もあるが、より簡単に実装できるようにするためのクラスが用意されている。

OpenJDK 12 の Windows 版 zip ファイルの、 SHA-256 のハッシュ値を実際に計算してみる。

jshell
jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

// ファイルの InputStream を生成
jshell> var in = new BufferedInputStream(new FileInputStream("openjdk-12_windows-x64_bin.zip"))
in ==> java.io.BufferedInputStream@10bbd20a

// DigestInputStream を生成
jshell> var dis = new DigestInputStream(in, md)
dis ==> [Digest Input Stream] SHA-256 Message Digest from SUN, <initialized>

// InputStream から全ての情報を読み取る(読み取り結果は必要ないので nullOutputStream() に捨てる)
jshell> dis.transferTo(OutputStream.nullOutputStream())
$31 ==> 196405895

// ハッシュ値を計算
jshell> var hash = md.digest()
hash ==> byte[32] { 53, -88, -48, 24, -12, 32, -5, 5, -2,  ... -13, -119, 124, 78, -110 }

DigestInputStream は、ストリームからデータを読み取るたびに、読み取ったデータが MessageDigestupdate() に渡される。

jshell
jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

// ファイルの InputStream を生成
jshell> var in = new BufferedInputStream(new FileInputStream("openjdk-12_windows-x64_bin.zip"))
in ==> java.io.BufferedInputStream@5a1c0542

// DigestOutputStream を生成(書き込まれた情報は必要ないので nullOutputStream() に捨てる)
jshell> var dos = new DigestOutputStream(OutputStream.nullOutputStream(), md)
dos ==> [Digest Output Stream] SHA-256 Message Digest from SUN, <initialized>

// InputStream から全ての情報を読み取り、 DigestOutputStream に書き出す
jshell> in.transferTo(dos)
$24 ==> 196405895

// ハッシュ値を計算
jshell> var hash = md.digest()
hash ==> byte[32] { 53, -88, -48, 24, -12, 32, -5, 5, -2,  ... -13, -119, 124, 78, -110 }

一方 DigestOutputStream は、ストリームにデータを書き出すたびに、書き出したデータが MessageDigestupdate() に渡される。

ちなみに、計算したハッシュ値を 16 進数文字列に変換すると、

jshell
jshell> toHexString(hash)
$12 ==> "35a8d018f420fb05fe7c2aa9933122896ca50bd23dbd373e90d8e2f3897c4e92"

OpenJDK のサイトで公開されているハッシュ値35a8d018f420fb05fe7c2aa9933122896ca50bd23dbd373e90d8e2f3897c4e92 なので、ちゃんと同じ値が計算できている。

暗号技術には様々な「鍵」が存在する(秘密鍵、公開鍵、プライベート鍵、etc...)。
JCA も、それに合わせて様々な鍵を表す型を定義している(SecretKey, PublicKey, PrivateKey, etc...)。

鍵を表す型は様々だが、これらは全て1つの共通のインターフェースを親に持っている。
それが Key インターフェースで、「鍵」を表す最上位の型となる。

Key には、鍵の情報にアクセスするためのメソッドが3つ定義されている。

  1. getAlgorithm()
    • AES や RSA といった、鍵に関連するアルゴリズム名を取得する
  2. getEncoded()
    • 鍵を特定の形式(X.509 や PKCS8 など)にエンコードした値を取得する
  3. getFormat()
    • エンコードの名前を取得する

鍵仕様

Key からはアルゴリズムなどの情報は取得できるが、鍵を構成する具体的なデータは取得できない。
このように具体的な鍵データにアクセスできないことを、 JCA では不透明な表現と呼んでいる。

一方で、鍵を構成する具体的なデータにアクセスできる型も用意されている。
それが KeySpec (鍵仕様)となる。

KeySpec 自体は「この型は鍵仕様である」ということを表現することが目的なので、実際に鍵データにアクセスするための API は定義されていない。
実際の API は、 KeySpec の実装クラスで定義されている。

例えば、 DSAPrivateKeySpec には、鍵生成で使用された素数 $p$ や非公開鍵 $x$ の情報にアクセスできるようになっている。

このように、鍵を構成する具体的なデータにアクセスできることを JCA では 透明な表現 と呼んでいる。

実際の KeyKeySpec および一部のクラス階層は次のような感じになっている。

jca.png

Generator と Factory

鍵を作るための手段として、 JCA には GeneratorFactory の2つが用意されている。

Generator は、鍵を新規に作成する機能を提供する。
例えば、鍵長のようなパラメータを指定して新しい鍵を生成できる。

一方 Factory は、主に鍵と鍵仕様を相互変換する機能を提供する。
鍵によっては異なる2つの鍵仕様から同じ鍵を生成できるものもある(らしい)。

具体的には次のようなクラスが存在する。

暗号化/復号

共通鍵暗号および公開鍵暗号による暗号化/復号処理は、どちらも javax.crypto.Cipher クラスを使用する。

Cipher のアルゴリズム指定

jshell
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher ==> Cipher.AES/CBC/PKCS5Padding, mode: not initialize ... orithm from: (no provider)

CiphergetInstance() に渡す文字列は単純なアルゴリズム名ではなく、ブロック暗号のモードなども指定できるようになっている。

具体的な書式は algorithm/mode/padding となる。

algorithm には AES や DES, RSA などの暗号化アルゴリズムの名前を指定する。
mode は、ブロック暗号のモードで、 ECB や CBC などを指定する。
padding は、暗号化対象のデータがブロックサイズの整数倍でなかったときに、足りない分をパディングする方法を指定する。

なお Cipher.getInstance("AES") のように algorithm だけを指定することも可能となっている。
この場合、モードとパディングはプロバイダが決めているデフォルト値が利用される。
多くの場合、デフォルトのモードは ECB になってしまうため、モードとパディングは常に明示しておいたほうが良い。

初期化

Cipher を使い始めるためには、まず先に初期化をしなければならない。

初期化には、「これから行う処理の種類」と「そのためのパラメータ」を指定する。
たとえば「暗号化」と「鍵」などを渡して初期化することになる。

jshell
// 鍵を生成
jshell> var keyGen = KeyGenerator.getInstance("AES")
keyGen ==> javax.crypto.KeyGenerator@365185bd

jshell> var key = keyGen.generateKey()
key ==> javax.crypto.spec.SecretKeySpec@fffe87d2

// Cipher の初期化
jshell> cipher.init(Cipher.ENCRYPT_MODE, key)

// 初期化パラメータの取得
jshell> var params = cipher.getParameters()
params ==>
    iv:
[0000: F1 D7 23 45 DA A6 7B 42   66 AF ...  46  ..#E...Bf.z..a.F
]

初期化には init() メソッドを使う。

第一引数には初期化の種類を指定する。
初期化の種類は、 Cipher に定義されている定数を使用する。
ENCRYPT_MODE は暗号化モードで、 DECRYPT_MODE は復号モードになる。

第二引数以降には初期化のパラメータを渡す。
基本は暗号化/復号で使う鍵を指定する。

暗号化のアルゴリズムによっては追加のパラメータが必要になる場合もある。
その場合は、 AlgorithmParameterSpec など追加のパラメータを受け取る init() メソッドを使用する。
AlgorithmParameterSpec は、アルゴリズム固有のパラメータを表すインターフェースで、様々な暗号化アルゴリズム固有の実装クラスが用意されている。

追加パラメータが必要なアルゴリズムでも、プロバイダがよしなにデフォルト値を設定してくれることがある。
その場合は、処理モードと鍵を受け取る init() メソッドだけで初期化ができる。
このとき使用されたパラメータは、 getParameters() メソッドで取得できる。

jshell
// Cipher の初期化
jshell> cipher.init(Cipher.ENCRYPT_MODE, key)

// 初期化パラメータの取得
jshell> var params = cipher.getParameters()
params ==>
    iv:
[0000: F1 D7 23 45 DA A6 7B 42   66 AF ...  46  ..#E...Bf.z..a.F
]

ここでは AES 用の鍵を生成して、 Cipher を暗号化モードで初期化している。

Cipher 生成時のモードを CBC にしていたため、本来は初期化ベクトル(IV : Initialization Vector)のパラメータが必要となる。
しかし、 init() で初期化ベクトルの指定を省略していたため、プロバイダがよしなに初期化ベクトルを生成してくれている。

初期化ベクトルは復号モードの初期化でも必要になるので getParameters() で取得しておく必要がある。
仮に使用した暗号化アルゴリズムが追加のパラメータを必要としなかった場合は、 getParameters()null を返す。

初期化を行うと、 Cipher は内部状態が全てリセットされる。
つまり、一度暗号化に使った Cipher のインスタンスを、今度は復号に再利用することができる。
逆にいうと、暗号化の途中で復号モードに初期化してしまうと、暗号化処理の途中で設定していた情報が失われるので注意が必要。

暗号化/復号

jshell
jshell> cipher.doFinal("Java Cryptography Architecture".getBytes())
$63 ==> byte[32] { -24, 54, 9, 79, -2, 118, 101, 69, -63, 30, -104, 77, -21, -24, -28, -4, 31, -56, -125, -121, 24, -115, 55, -92, -68, -35, -6, 90, -108, -122, -16, 21 }

doFinal() メソッドに暗号化/復号したい値(byte 配列)を渡すことで、暗号文/平文(byte 配列)を得ることができる。

MessageDigest と同様で、入力は update() を使って分割することもできる。
ただし、 MessageDigest の場合とは異なり暗号化/復号の結果は update() のたびに返される。

jshell
jshell> cipher.update("Java ".getBytes())
$59 ==> byte[0] {  }

jshell> cipher.update("Cryptography ".getBytes())
$60 ==> byte[16] { -24, 54, 9, 79, -2, 118, 101, 69, -63, 30, -104, 77, -21, -24, -28, -4 }

jshell> cipher.update("Architecture".getBytes())
$61 ==> byte[0] {  }

jshell> cipher.doFinal()
$62 ==> byte[16] { 31, -56, -125, -121, 24, -115, 55, -92, -68, -35, -6, 90, -108, -122, -16, 21 }

さらにブロック暗号を使っている場合、入力サイズがブロックのサイズになるまでは空の byte 配列が返される。
AES の場合、ブロックのサイズは 128 ビット(16 バイト)なので、入力が 16 バイトに達するまでは update() の戻り値は空配列になる。

doFinal() が呼ばれると、 Cipher のインスタンスは直前の初期化時に戻る。
つまり、そのまま別の入力を暗号化/復号するのに再利用できる。

CipherInputStream/CipherOutputStream

DigestInputStream/DigestOutputStream と同じように、 CipherInputStream/OutputStream を連携させたクラスが用意されている。

それぞれのデータと暗号化/復号の流れは、下図のようなイメージになる。

jca.jpg

まずは CipherInputStream を使った場合。

jshell
// 暗号化対象となる InputStream を作成
jshell> var is = new ByteArrayInputStream("Java Cryptography Architecture".getBytes())
is ==> java.io.ByteArrayInputStream@5fcd892a

// CipherInputStream を作成
jshell> var cis = new CipherInputStream(is, cipher)
cis ==> javax.crypto.CipherInputStream@b9afc07

// 出力先の OutputStream を作成
jshell> var out = new ByteArrayOutputStream()
out ==>

// InputStream から全情報を取り出し、暗号化結果を OutputStream に書き出す
jshell> cis.transferTo(out)
$67 ==> 32

// OutputStream に書き出された結果を確認
jshell> out.toByteArray()
$68 ==> byte[32] { -24, 54, 9, 79, -2, 118, 101, 69, -63, 30, -104, 77, -21, -24, -28, -4, 31, -56, -125, -121, 24, -115, 55, -92, -68, -35, -6, 90, -108, -122, -16, 21 }

次に CipherOutputStream を使った場合。

jshell
// 暗号化対象となる InputStream を作成
jshell> var is = new ByteArrayInputStream("Java Cryptography Architecture".getBytes())
is ==> java.io.ByteArrayInputStream@133e16fd

// 暗号文の出力先となる OutputStream を作成
jshell> var out = new ByteArrayOutputStream()
out ==>

// CipherOutputStream を作成
jshell> var cos = new CipherOutputStream(out, cipher)
cos ==> javax.crypto.CipherOutputStream@51b279c9

// 暗号化対象を CipherOutputStream に流し込む
jshell> is.transferTo(cos)
$92 ==> 30

// doFinal() を実行させるため CipherOutputStream を close()
jshell> cos.close()

// 暗号文を確認
jshell> out.toByteArray()
$94 ==> byte[32] { -24, 54, 9, 79, -2, 118, 101, 69, -63, 30, -104, 77, -21, -24, -28, -4, 31, -56, -125, -121, 24, -115, 55, -92, -68, -35, -6, 90, -108, -122, -16, 21 }

CipherInputStreamCipherOutputStream は、どちらも close() 時に CipherdoFinal() が実行される。
なので、 close() は忘れずに実行しておかないと結果が中途半端になったりする1(ストリームなので、基本 try-with-resources などを使うから気にする必要はないと思うが)。

transferTo() は使うべきでない?

CipherInputStreamJavadoc には次のように書かれている。

このクラスを使用するプログラマは、このクラスで定義されていないメソッド、またはオーバーライドされていないメソッド(あとでスーパー・クラスのいずれかに追加された新しいメソッドやコンストラクタなど)を絶対に使用しないでください。それらのメソッドの設計と実装では、CipherInputStreamに関するセキュリティ上の影響が考慮されていない可能性があるためです。

一方、 InputStream には Java 9 で transferTo() という便利なメソッドが追加されている。

この transferTo()CipherInputStream でオーバーライドされていないので、使うべきでない条件を満たしてしまっている。

ただ、この transferTo() は OpenJDK 11 では次のように実装されている。

InputStream.java
    public long transferTo(OutputStream out) throws IOException {
        Objects.requireNonNull(out, "out");
        long transferred = 0;
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;
        while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
            out.write(buffer, 0, read);
            transferred += read;
        }
        return transferred;
    }

ここでは、 InputStreamread()OutputStreamwrite() だけが使われている。

もし transferTo() を使わずに処理を書こうとしても、結局この transferTo() がやっていることと同じ実装を書くことになる。
そして、そのとき使用するメソッドは CipherInputStream がオーバーライドしている read()CipherOutputStreamwrite() になる。

となると、結局 transferTo() を使っても問題ないと個人的には思う。

ただし、この実装はあくまで OpenJDK に限った話なので、他の Java 実装だとセキュリティ上問題のある実装になっている可能性もゼロではないかもしれない。

ご利用は自己責任で。

AES

AES の暗号化と復号を実装してみる。

鍵の生成

jshell
// アルゴリズムを AES にして KeyGenerator を作成
jshell> var keyGen = KeyGenerator.getInstance("AES")
keyGen ==> javax.crypto.KeyGenerator@4d50efb8

// 鍵の生成
jshell> var key = keyGen.generateKey()
key ==> javax.crypto.spec.SecretKeySpec@177c6

鍵の生成には KeyGenerator を使用する。
SunJCE プロバイダでは、デフォルトの鍵長は 128 ビットになる。

鍵長の指定

jshell
jshell> keyGen.init(256)

init() メソッドで鍵長を指定できる。指定可能な鍵長は 128, 192, 256 のいずれかのみ(AES の仕様)。

暗号化

jshell
// Cipher を AES で生成
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher ==> Cipher.AES/CBC/PKCS5Padding, mode: not initialize ... orithm from: (no provider)

// 暗号化モードで初期化
jshell> cipher.init(Cipher.ENCRYPT_MODE, key)

// 初期化ベクトルの情報を取得
jshell> var params = cipher.getParameters()
params ==>
    iv:
[0000: 88 8E 06 A0 89 80 0D 11   FF ED ...  45  ...........&.'.E
]

// 暗号化
jshell> var c = cipher.doFinal("Java Cryptography Architecture".getBytes())
c ==> byte[32] { -35, -80, -57, 91, -75, -46, -123, 49, ...  22, 90, -121, 124, -108 }

// 暗号化できているか確認
jshell> new String(c)
$24 ==> "ンーヌ[オメ?1juセエヒ「リ\017}初ョPW」\026Z?|?"

復号時に初期化ベクトルの情報が必要になるので、 getParameters() でパラメータの情報を取得しておく。

復号

jshell
// 復号モードで初期化
jshell> cipher.init(Cipher.DECRYPT_MODE, key, params)

// 復号
jshell> var p = cipher.doFinal(c)
p ==> byte[30] { 74, 97, 118, 97, 32, 67, 114, 121, 112 ... , 99, 116, 117, 114, 101 }

// 復号できているか確認
jshell> new String(p)
$23 ==> "Java Cryptography Architecture"

init() のときに、鍵だけでなく暗号化の初期化で使用したパラメータも指定しなければならない。

鍵をファイルに出力する

jshell
// 鍵をファイルに出力
jshell> writeFile("secret-key", key.getEncoded())

// 暗号化時に使用したパラメータをファイルに出力
jshell> writeFile("aes-params", params.getEncoded())

// 暗号文の生成
jshell> var c = cipher.doFinal("Hello World!!".getBytes())
c ==> byte[16] { -77, 124, 124, -4, -92, 80, -89, 102,  ... 1, 114, 7, 117, -104, 11 }

// 暗号文をファイルに出力
jshell> writeFile("aes-cryptogram", c)

秘密鍵のエンコードされたデータは KeygetEncoded() で取得できる。
また、パラメータの情報は AlgorithmParametersgetEncoded() で取得できる。

鍵をファイルから復元する

jshell
// 秘密鍵の情報をファイルから読み取り、 SecretKeySpec を生成する
jshell> Key key = new SecretKeySpec(readFile("secret-key"), "AES")
key ==> javax.crypto.spec.SecretKeySpec@178a8

// AlgorithmParameters のインスタンスを取得し、ファイルから読み込んだ情報で初期化する
jshell> var params = AlgorithmParameters.getInstance("AES")
params ==>

jshell> params.init(readFile("aes-params"))

// Cipher を生成して読み込んだ情報で初期化
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher ==> javax.crypto.Cipher@df27fae

jshell> cipher.init(Cipher.DECRYPT_MODE, key, params)

// 暗号文をファイルから読み取り、復号
jshell> var m = cipher.doFinal(readFile("aes-cryptogram"))
m ==> byte[13] { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 33 }

// 結果を確認
jshell> new String(m)
$34 ==> "Hello World!!"

秘密鍵は SecretKeySpec を使って復元できる(アルゴリズムは "AES" を指定)。
このクラスは KeySpec でありながら Key も実装しているため、このまま鍵として利用できる。

パラメータは AlgorithmParameters で復元できる。
まずは getInstance()"AES" を指定してインスタンスを取得する。
そして、 init() でパラメータのデータを設定する。

パスワードベース暗号(PBE)

jshell
// パスワードベース暗号用の KeySpec を生成
jshell> char[] password = { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }
password ==> char[8] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }

jshell> var keySpec = new PBEKeySpec(password)
keySpec ==> javax.crypto.spec.PBEKeySpec@5a8e6209

// SecretKeyFactory を使って KeySpec から Key を生成
jshell> var keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_128")
keyFac ==> javax.crypto.SecretKeyFactory@3234e239

jshell> var key = keyFac.generateSecret(keySpec)
key ==> com.sun.crypto.provider.PBEKey@855e49d8

PBE では、 PBEKey という鍵を使用する。

PBEKey は、パスワードをもとに PBEKeySpec を生成し、SecretKeyFactory を使って PBEKey に変換して取得する。

パスワードは char の配列で指定しなければならない。
これは、 PBEKeySpec の Javadoc に理由が書いてあるが、 String だと値が不変であるためあとでクリアができないかららしい。

SecretKeyFactory のインスタンス取得では、アルゴリズムに PBEWithHmacSHA256AndAES_128 を指定している。

アルゴリズム名は PBEWith<digest|prf>And<encryption> という書式になっている。
digest|prf が MD5 や SHA1 の場合、 RFC8018 でいうところの PBES1 になり、 Hmac* の場合は PBES2 を使うことになる。

jshell
// パスワードベース暗号用の Cipher を生成して暗号化モードで初期化
jshell> var cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_128/CBC/PKCS5Padding")
cipher ==> javax.crypto.Cipher@5891e32e

Cipher も同じアルゴリズムでインスタンスを取得する。
(SunJCE プロバイダの Cipher が PBE でサポートしているモードとパディングは CBC/PKCS5Padding のみなので明示的に指定しなくてもよさそうな気がするが、念の為)

jshell
// 疑似乱数生成器の作成
jshell> var random = new SecureRandom()
random ==> Hash_DRBG,SHA-256,128,reseed_only

// salt の生成
jshell> var salt = new byte[64];
salt ==> byte[64] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... , 0, 0, 0, 0, 0, 0, 0, 0 }

jshell> random.nextBytes(salt)

ソルトを生成する。

ソルトは暗号論的疑似乱数生成器(SecureRandom)を使って生成する。

RFC8018 ではソルトのサイズを最低 64 ビット以上としているので、とりあえず 64 ビットで生成した。

jshell
// イテレーション回数の宣言
jshell> int iterationCount = 1000
iterationCount ==> 1000

// パスワードベース暗号用の AlgorithmParameterSpec を生成
jshell> var keyParamSpec = new PBEParameterSpec(salt, iterationCount)
keyParamSpec ==> javax.crypto.spec.PBEParameterSpec@eec5a4a

jshell> cipher.init(Cipher.ENCRYPT_MODE, key, keyParamSpec)

イテレーション回数とソルトの値を使って、 PBEParameterSpec を生成する。

そして、先程作った鍵とともに Cipher の初期化を行う。

jshell
// 暗号化実行
jshell> var c = cipher.doFinal("Hello World!!".getBytes())
c ==> byte[16] { 101, -52, -106, 26, 67, 118, -20, 22,  ...  42, -71, 115, 123, -122 }

// 暗号結果を確認
jshell> new String(c)
$18 ==> "eフ?\032Cv?\026\031ャ\000*ケs{?"

あとは、他のアルゴリズムと同じように暗号化を行う。

jshell
// 暗号化時に使用したパラメータを取得
jshell> var params = cipher.getParameters()
params ==> PBEWithHmacSHA256AndAES_128

jshell> PBEParameterSpec pbeParamSpec = params.getParameterSpec(PBEParameterSpec.class)
pbeParamSpec ==> javax.crypto.spec.PBEParameterSpec@35a50a4c

// IvParameterSpec を取得
jshell> IvParameterSpec ivSpec = (IvParameterSpec)pbeParamSpec.getParameterSpec()
ivSpec ==> javax.crypto.spec.IvParameterSpec@281e3708

// IV (初期化ベクトル)の値を確認
jshell> var iv = ivSpec.getIV()
iv ==> byte[16] { -67, 47, 111, -2, 34, -17, -89, 74, -7 ... 46, -124, 99, 79, 23, 88 }

CBC で暗号化したので、復号時のために初期化ベクトルを取得しておく必要がある。
暗号化時に自動設定された初期化ベクトルの情報(IvParameterSpec)は、 PBEParameterSpecgetParameterSpec() メソッドで取得できる。

jshell
// 復号用に PBEParameterSpec を生成
jshell> var ivSpec = new IvParameterSpec(iv)
ivSpec ==> javax.crypto.spec.IvParameterSpec@dbd940d

jshell> PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, iterationCount, ivSpec)
pbeParamSpec ==> javax.crypto.spec.PBEParameterSpec@17695df3

// 復号モードで初期化(※key の生成は暗号化時と同じ)
jshell> cipher.init(Cipher.DECRYPT_MODE, key, pbeParamSpec)

// 復号を実行
jshell> var m = cipher.doFinal(c)
m ==> byte[13] { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 33 }

// 復号結果を確認
jshell> new String(m)
$21 ==> "Hello World!!"

復号時は、再び「パスワード」「ソルト」「イテレーション回数」の情報を使って KeyAlgorithmParameterSpec を生成し、 Cipher を初期化する。
(IV の情報は暗号化時のものを使う必要があるので、 PBEParameterSpec の生成は若干異なっている)

RSA

公開鍵暗号の RSA を使用してみる。

鍵の生成

jshell
// 鍵ペアを生成するための KeyPairGenerator を取得
jshell> var keyPairGen = KeyPairGenerator.getInstance("RSA")
keyPairGen ==> java.security.KeyPairGenerator$Delegate@42dafa95

// 鍵ペアを生成
jshell> var keyPair = keyPairGen.generateKeyPair()
keyPair ==> java.security.KeyPair@1dfe2924

公開鍵暗号の鍵ペアを生成するには、 KeyPairGenerator を使用する。
RSA を使うので、アルゴリズム名は "RSA" を渡す。

generateKeyPair() を実行すると、公開鍵とプライベート鍵のセットを含んだ KeyPair を取得できる。

鍵長の指定

jshell
jshell> keyPairGen.initialize(4096)

jshell> keyPairGen.generateKeyPair().getPublic()
$9 ==> Sun RSA public key, 4096 bits
  params: null
  modulus: 58218231801097229661983999673649965349728454764714757172625245890343045...
  public exponent: 65537

KeyPairGeneratorinitialize() メソッドで鍵長を指定できる。

暗号化

jshell
// RSA 用の Cipher を取得
jshell> var cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
cipher ==> javax.crypto.Cipher@35a50a4c

// 暗号化モードで初期化
jshell> cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic())

// 暗号化実行
jshell> var c = cipher.doFinal("Hello World!!".getBytes())
c ==> byte[256] { 44, 48, -56, -42, -117, -48, 17, -76, ... , -15, 63, 46, -115, -42 }

// 暗号結果を確認
jshell> new String(c)
$14 ==> ",0ネヨ巾\021エ\030?0\022:リ斃w?9ァ,萬|ノ_?轍モォノmトЩア]\002錥)マ\024L5XD?齡ツル訶({x?シ\024,5、S@wM-?杣ウg1ン\025「02鷦ヘラトJ盆ミ?\tゥレmフF悊^?\"ミウ\023ゥ&モホ輙モ瓸コ.Mri\037クi?9ムEDケレgg\023q亥X4\021pM3廱ヤ塗覆ホ\017G\024億ア\017ソ?;ィ Q\177冬U5wgL゙B浅簗涓ミ>\006e?g?9lエ醋棒:\t]垉R\020ソ?nミ?!?&sエ?ォラ\016`H\f6漬?レ?ュ4\nfU呶?.斎"

KeyPairgetPublic() で公開鍵を取得できるので、これで暗号化を行う。
手順は AES の場合と一緒。

復号

jshell
// 暗号化時に生成されたパラメータを取得
jshell> var params = cipher.getParameters()
params ==> MD: SHA-256
MGF: MGF1SHA-1
PSource: PSpecified

// 復号モードで初期化
jshell> cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate(), params)

// 復号実行
jshell> var m = cipher.doFinal(c)
m ==> byte[13] { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 33 }

// 復号結果を確認
jshell> new String(m)
$18 ==> "Hello World!!"

KeyPairgetPrivate() でプライベート鍵が取得できるので、それを使って復号を行う。
こちらも、 Cipher の使い方は AES の場合と同じ感じ。

メッセージ認証コード(MAC)

jshell
// MAC 用に秘密鍵を生成する
jshell> var keyGen = KeyGenerator.getInstance("HmacSHA256")
keyGen ==> javax.crypto.KeyGenerator@17579e0f

jshell> var key = keyGen.generateKey()
key ==> javax.crypto.spec.SecretKeySpec@588390c

// MAC のインスタンスを生成する
jshell> var mac = Mac.getInstance("HmacSHA256")
mac ==> javax.crypto.Mac@7a765367

// MAC を初期化する
jshell> mac.init(key)

// MAC 値を計算する
jshell> var mv = mac.doFinal("Hello World!!".getBytes())
mv ==> byte[32] { 0, -103, -60, 37, -74, 97, 115, -50, 7 ... 80, 57, -84, -59, 90, 23 }

MAC 値の計算には Mac クラスを使用する。

MAC には秘密鍵が必要になるので、 KeyGenerator を使って鍵を生成する。

Mac のインスタンスを生成したら、まずは秘密鍵を使って init() メソッドで初期化を行う。
次に、 doFinal() に MAC 値を計算したい値(byte 配列)を渡す。
計算された MAC 値は、 byte 配列として返される。

分割して計算する

jshell
jshell> mac.update("Hello ".getBytes())

jshell> mac.update("World!!".getBytes())

jshell> mac.doFinal()
$9 ==> byte[32] { 0, -103, -60, 37, -74, 97, 115, -50, 77, 6, 107, 27, 15, -41, -15, 111, -99, -50, -85, -68, 11, -66, 69, -54, 44, -92, 80, 57, -84, -59, 90, 23 }

Cipher などと同様に、 update() を使って計算対象の値を分割して設定できる。

デジタル署名

RSA 署名

jshell
// RSA の鍵ペアを生成する
jshell> var keyPairGen = KeyPairGenerator.getInstance("RSA")
keyPairGen ==> java.security.KeyPairGenerator$Delegate@42dafa95

jshell> var keyPair = keyPairGen.generateKeyPair()
keyPair ==> java.security.KeyPair@1dfe2924

まずは、 RSA 暗号のときと同じように RSA の鍵ペアを生成する。

jshell
// Signature インスタンス取得
jshell> var signature = Signature.getInstance("SHA256WithRSA")
signature ==> Signature object: SHA256WithRSA<not initialized>

// 署名モードで初期化
jshell> signature.initSign(keyPair.getPrivate())

// 署名対象のデータを登録
jshell> signature.update("Hello World!!".getBytes())

// デジタル署名を生成
jshell> var sign = signature.sign()
sign ==> byte[256] { 29, -91, 95, 8, -19, -56, 118, -29, 1 ... 7, -1, 53, 58, -99, -117 }

デジタル署名を作成するには、 Signature クラスを使用する。

Signature は2つの初期化メソッドを持っている。
1つが initSign() で、もう1つが initVerify() になる。

署名を作成する場合は、 initSign() を使って初期化を行う。
このとき、引数には署名で使用する署名鍵(プライベート鍵)を渡す。

初期化が済んだら、次は update() メソッドで署名対象のデータを登録していく。
最後に sign() メソッドを実行すると、デジタル署名が byte 配列で返される。

jshell
// Signature を検証モードで初期化
jshell> signature.initVerify(keyPair.getPublic())

// 検証対象のデータを登録
jshell> signature.update("Hello World!!".getBytes())

// 検証を実行
jshell> signature.verify(sign)
$12 ==> true

検証を行う場合は、 initVerify()Signature を初期化する。
このとき、引数には検証鍵(公開鍵)を渡す。

こちらも update() で検証対象のデータを登録する。
そして、最後に verify() メソッドで検証を実行する。
このとき、引数にはデジタル署名の byte 配列を渡す。

検証が成功した場合は true が返る。

DSA 署名

jshell
// DSA 用の鍵ペアを生成
jshell> var keyPairGen = KeyPairGenerator.getInstance("DSA")
keyPairGen ==> sun.security.provider.DSAKeyPairGenerator$Current@f6c48ac

jshell> var keyPair = keyPairGen.generateKeyPair()
keyPair ==> java.security.KeyPair@1f36e637

// DSA 用の Signature インスタンスを取得
jshell> var signature = Signature.getInstance("SHA256WithDSA")
signature ==> Signature object: SHA256WithDSA<not initialized>

// 署名モードで初期化
jshell> signature.initSign(keyPair.getPrivate())

// 署名対象データを登録
jshell> signature.update("Hello World!!".getBytes())

// デジタル署名を生成
jshell> var sign = signature.sign()
sign ==> byte[63] { 48, 61, 2, 29, 0, -72, 46, -94, 42, 12 ... 4, 59, -38, -5, -48, 127 }

// 検証モードで初期化
jshell> signature.initVerify(keyPair.getPublic())

// 検証対象データを登録
jshell> signature.update("Hello World!!".getBytes())

// 検証を実行
jshell> signature.verify(sign)
$12 ==> true

アルゴリズム名などを DSA 用に変えただけで、実装の方法は RSA の場合と同じ感じでできる。

keytool

keytool | Java Platform, Standard Editionツール・リファレンス

JDK には keytool というコマンドラインツールが同梱されている。
keytool を使うと、鍵や証明書を生成したり管理することができる。

keytoolのヘルプ
$ keytool -h
キーおよび証明書管理ツール

コマンド:

 -certreq            証明書リクエストを生成します
 -changealias        エントリの別名を変更します
 -delete             エントリを削除します
 -exportcert         証明書をエクスポートします
 -genkeypair         鍵ペアを生成します
 -genseckey          秘密鍵を生成します
 -gencert            証明書リクエストから証明書を生成します
 -importcert         証明書または証明書チェーンをインポートします
 -importpass         パスワードをインポートします
 -importkeystore     別のキーストアから1つまたはすべてのエントリをインポートします
 -keypasswd          エントリの鍵パスワードを変更します
 -list               キーストア内のエントリをリストします
 -printcert          証明書の内容を出力します
 -printcertreq       証明書リクエストの内容を出力します
 -printcrl           CRLファイルの内容を出力します
 -storepasswd        キーストアのストア・パスワードを変更します

このヘルプ・メッセージを表示するには"keytool -?、-hまたは--help"を使用します
command_nameの使用方法については、"keytool -command_name --help"を使用します。
事前構成済のオプション・ファイルを指定するには、-conf <url>オプションを使用します。

コマンドの書式

keytool <コマンド> <オプション>

keytool は、実行するコマンドと、そのオプションを渡すことで利用できる。
コマンドとオプションは、全て -名前 という形式になっている。

コマンドの一覧は -h で確認できる。
さらに各コマンドのヘルプは、そのコマンドのオプションに -h を渡すことで詳細を確認できる。

-certreqコマンドのヘルプ
$ keytool -certreq -h
keytool -certreq [OPTION]...

証明書リクエストを生成します

オプション:

 -alias <alias>          処理するエントリの別名
 -sigalg <alg>           署名アルゴリズム名
 -file <file>            出力ファイル名
 -keypass <arg>          鍵のパスワード
 -keystore <keystore>    キーストア名
 -dname <name>           識別名
 -ext <value>            X.509拡張
 -storepass <arg>        キーストアのパスワード
 -storetype <type>       キーストアのタイプ
 -providername <name>    プロバイダ名
 -addprovider <name>     名前でセキュリティ・プロバイダを追加する(SunPKCS11など)
   [-providerarg <arg>]    -addproviderの引数を構成する
 -providerclass <class>  完全修飾クラス名でセキュリティ・プロバイダを追加する
   [-providerarg <arg>]    -providerclassの引数を構成する
 -providerpath <list>    プロバイダ・クラスパス
 -v                      詳細出力
 -protected              保護メカニズムによるパスワード

このヘルプ・メッセージを表示するには"keytool -?、-hまたは--help"を使用します

キーストア

keytool はキーストアと呼ばれるファイルに鍵や証明書などの情報を保存する。
キーストアは、 Java 8 までは JKS という Oracle 独自のファイルフォーマットで作成されていた。
Java 9 以降は PKCS#12 という規格(RFC7292)で定義されているファイルフォーマットで作成される。

keytool の各コマンドは -keystore というオプションを受け取るようになっている。
このオプションで、キーストアのファイルの場所を指定する。
未指定の場合は、デフォルトで ホームディレクトリ/.keystore がキーストアファイルのパスとして使用される。

コマンドの一覧を見るとわかるが、キーストアを新規作成するための専用のコマンドは存在しない。
キーストアファイルは、最初に鍵などを生成しようとしたときに、ファイルが無ければ自動的に作成されるようになっている。

キーストア自体はパスワードベース暗号で暗号化されていて、中身にアクセスするためにはパスワードの入力が必要になる。
キーストアのパスワードは、そのキーストアを最初に作成するときに入力を求められる。
パスワードの長さは 6 桁以上でなければならない。

キーストアの情報は、Java プログラムからもアクセスできるようになっている。
そのためのクラスが KeyStore になる(詳細後述)。

エントリ

キーストア内に格納する鍵や証明書は、総称してエントリと呼ばれる。

キーストアは一種のキーバリューストアになっていて、各エントリにはエイリアスと呼ばれる名前をつけて管理する。
特定のエントリの情報を表示・編集するためには、エイリアスでエントリを指定することになる。

エントリの種類

エントリは2種類存在する。

  • 鍵のエントリ
  • 信頼された証明書のエントリ

「鍵のエントリ」は、秘密鍵か、プライベート鍵とそのペアとなる公開鍵証明書のセットを格納したエントリになる。
公開鍵証明書は自己署名証明書の場合もあれば、ルート証明書までチェーンしている場合もある。

一方「信頼できる証明書のエントリ」には単独の公開鍵証明書が格納される。
他の公開鍵によって署名されている場合も、このエントリに格納される公開鍵証明書は1つだけになる(チェーンはしていない)。

キーストアの中身を確認する

一覧の表示

$ keytool -list
キーストアのタイプ: PKCS12
キーストア・プロバイダ: SUN

キーストアには8エントリが含まれます

hogekey,2019/04/10, SecretKeyEntry,
pbekey,2019/04/10, SecretKeyEntry,
rsakey,2019/04/13, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): CB:D1:0C:85:5F:1D:50:CC:62:C6:68:11:9F:7E:7D:4D:F7:1B:45:40:44:71:99:B3:41:2B:71:7A:E1:A6:02:FB
dsakey,2019/04/13, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): E9:70:1D:B2:8C:8B:18:C2:7C:A4:A0:DC:32:A5:37:06:ED:1B:DF:30:65:32:77:B5:43:77:4D:9D:42:15:70:B2
qiita-cert,2019/04/15, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 57:28:2C:9D:D0:43:19:08:A4:CC:D3:52:CF:5F:32:16:EC:9D:DA:4A:4E:D1:5C:1F:3A:EB:39:3F:76:A3:91:D4

-list コマンドでエントリの一覧を確認できる。

PrivateKeyEntry と書いてるのが、プライベート鍵と公開鍵のペアが格納されたエントリになる(鍵エントリ)。
SecretKeyEntry と書いてるのが、秘密鍵のエントリになる(こちらも鍵エントリ)。
trustedCertEntry と書いてるのが、信頼できる証明書のエントリになる。

エントリを指定して表示

$ keytool -list -alias hogekey
hogekey,2019/04/10, SecretKeyEntry,

$ keytool -list -alias rsakey
rsakey,2019/04/13, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): CB:D1:0C:85:5F:1D:50:CC:62:C6:68:11:9F:7E:7D:4D:F7:1B:45:40:44:71:99:B3:41:2B:71:7A:E1:A6:02:FB

-alias オプションで個別に表示したいエントリのエイリアスを指定すると、そのエイリアスの情報だけを出力できる。

さらに -v オプションをつけることで詳細な情報を出力できる。

$ keytool -list -alias hogekey -v
別名: hogekey
作成日: 2019/04/10
エントリ・タイプ: SecretKeyEntry

$ keytool -list -alias rsakey -v
別名: rsakey
作成日: 2019/04/13
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 1
証明書[1]:
所有者: CN=Alice, C=JP
発行者: CN=Alice, C=JP
シリアル番号: 6a59c918
有効期間の開始日: Sat Apr 13 08:41:13 JST 2019終了日: Fri Jul 12 08:41:13 JST 2019
証明書のフィンガプリント:
         SHA1: 1F:11:0E:8C:30:0B:DA:D8:2C:79:AD:C7:4B:2B:70:62:85:94:94:CA
         SHA256: CB:D1:0C:85:5F:1D:50:CC:62:C6:68:11:9F:7E:7D:4D:F7:1B:45:40:44:71:99:B3:41:2B:71:7A:E1:A6:02:FB
署名アルゴリズム名: SHA256withRSA
サブジェクト公開鍵アルゴリズム: 2048ビットRSA鍵
バージョン: 3

拡張:

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 92 68 47 49 D0 26 A7 1D   04 51 64 3E 96 5B B3 05  .hGI.&...Qd>.[..
0010: 40 7B 3B E2                                        @.;.
]
]

キーストアのパスを変更する

$ keytool -storepasswd
新規keystore password:
新規keystore passwordを再入力してください:

-storepasswd コマンドで、キーストアのパスワードを変更できる。

エントリを削除する

$ keytool -delete -alias foo

-delete コマンドで、任意のエントリを削除できる。

削除対象のエントリは、 -alias オプションで指定する。

エントリのエイリアスを変更する

$ keytool -changealias -alias from-alias -destalias to-alias

-changealias コマンドで、任意のエントリのエイリアスを変更できる。

-alias は、変更対象のエイリアスを指定する。
-destalias は、変更後のエイリアスの名前を指定する。

JRE に組み込まれているキーストアを確認する

Java は実行環境にルート証明書を記録したキーストアを保持している。

具体的なファイルは ${JAVA_HOME}/lib/security/cacerts になる。
これもキーストアなので、 keytool を使って内容を確認したり編集したりできる。

cacertsの内容を確認する
$ keytool -list -cacerts
キーストアのパスワードを入力してください:
キーストアのタイプ: JKS
キーストア・プロバイダ: SUN

キーストアには93エントリが含まれます

verisignclass2g2ca [jdk],2018/06/13, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 3A:43:E2:20:FE:7F:3E:A9:65:3D:1E:21:74:2E:AC:2B:75:C2:0F:D8:98:03:05:BC:50:2C:AF:8C:2D:9B:41:A1
digicertassuredidg3 [jdk],2017/12/01, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 7E:37:CB:8B:4C:47:09:0C:AB:36:55:1B:A6:F4:5D:B8:40:68:0F:BA:16:6A:95:2D:B1:00:71:7F:43:05:3F:C2
verisignuniversalrootca [jdk],2017/12/01, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 23:99:56:11:27:A5:71:25:DE:8C:EF:EA:61:0D:DF:2F:A0:78:B5:C8:06:7F:4E:82:82:90:BF:B8:60:E8:4B:3C

...

証明書のフィンガプリント(SHA-256): 43:48:A0:E9:44:4C:78:CB:26:5E:05:8D:5E:89:44:B4:D8:4F:96:62:BD:26:DB:25:7F:89:34:A4:43:C7:01:61
addtrustqualifiedca [jdk],2017/12/01, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 80:95:21:08:05:DB:4B:BC:35:5E:44:28:D8:FD:6E:C2:CD:E3:AB:5F:B9:7A:99:42:98:8E:B8:F4:DC:D0:60:16

デフォルトのパスワードは changeit となっている。
ちなみに、ドキュメントには「システム管理者は、SDKのインストール後、このファイルのパスワードとデフォルト・アクセス権を変更する必要があります。」と書かれている。

オプションの -cacerts を指定すると、 cacerts ファイルをキーストアとして指定したのと同じ状態になるので、 -keystore でファイルパスを指定しなくてもアクセスできる。

Java で SSL 通信などを実装すると、デフォルトではこの cacerts を使って証明書の検証が行われる。
また、後述する証明書のインポートのときにも参照される場合がある。

秘密鍵を生成する

$ keytool -genseckey -keyalg AES -keysize 256 -alias HogeKey
キーストアのパスワードを入力してください:**********
新規パスワードを再入力してください:**********

共通鍵暗号の秘密鍵を生成するには、 -genseckey コマンドを使用する。

パスワードの入力を求められているが、これはキーストアファイル自体を暗号化/復号するためのパスワードになる。
特に今回は初回アクセスだった(キーストアを初めて作成した)ので、新規パスワードの再入力も促されている。
次回以降は、パスワードの入力は一回だけになる。

-keyalg で鍵のアルゴリズムとして AES を指定し、鍵長を 256 としている。

-alias では、登録される秘密鍵につけるエイリアスを指定している。
省略可能だが、その場合は mykey という値になる。

パスワードベース鍵は生成できない?

-keyalg にパスワードベース鍵のアルゴリズムを指定してエントリを作成してみた。

$ keytool -genseckey -keyalg PBEWithHmacSHA256AndAES_128 -keysize 256 -alias pbeKey
キーストアのパスワードを入力してください:*******
保存するパスワードを入力してください:**************
パスワードを再入力してください:**************

特にエラーもなくエントリが生成できたので、成功した感じがする。
しかし、このエントリを Java プログラムから取得すると、次のようになる。

jshell
前略

jshell> var entry = keystore.getEntry("pbeKey", password)
entry ==> Secret key entry with algorithm PBEWithMD5AndDES

アルゴリズムが PBEWithMD5AndDES になってる。。。
正式なサポートはされていないということだろうか?

公開鍵暗号の鍵ペアを生成する

$ keytool -genkeypair -keyalg RSA -keysize 2048 -dname CN=Alice,C=JP -alias rsakey

公開鍵暗号の鍵ペアを生成するには、 -genkeypair コマンドを使用する。

共通鍵暗号のときと同様で、 -keyalg で鍵のアルゴリズムを、 -keysize で鍵長(ビット数)を指定できる。
(DSA の鍵ペアを作るなら、 -keyalgDSA と指定すればいい)

-genkeypair では、公開鍵は自己署名証明書(X.509)の形で生成される。
-dname では、証明書の発行者(issuer)と所有者(subject)に設定される X.500 識別名を指定する。
-dname の指定を省略した場合は、コマンドラインから X.500 識別名の入力を求められる。

X.500 識別名

X.500 識別名は、 CN=Amazon, OU=Server CA 1B, O=Amazon, C=US のように <属性名>=<値> をカンマで区切った形で記述する。
属性名には次の値を指定できる。

属性名 意味
CN 一般名(common name)
所有者の名前など
OU 部門名(organization unit)
部や課などの名前
O 組織名(organization name)
会社名など
L 地域名(locality name)
都市名など
S 地方名(state name)
州名など(日本なら都道府県レベル?)
C 国コード(country)
国を識別する2桁のコード(JP, US など)

実際の例をいくつか。

確認したサイト subjectの値
Qiita
https://qiita.com/
CN=qiita.com
日本オラクル
https://www.oracle.com/jp/index.html
CN=www-cs-01.oracle.com
OU=Content Management Services IT
O=Oracle Corporation
L=Redwood Shores
S=California
C=US
Amazon
https://www.amazon.co.jp/
CN=www.amazon.co.jp
O=Amazon.com, Inc.
L=Seattle
S=Washington
C=US
内閣府
https://www.cao.go.jp/
CN=*.cao.go.jp
O=Cabinet Office
L=Chiyoda-ku
S=Tokyo
C=JP
Panasonic
https://panasonic.jp/
CN=panasonic.jp
O=Panasonic Corporation
L=Kadoma
S=Osaka
C=JP

各属性名は、全てを指定する必要はない。
ただし、順序には制約があり、 CN, OU, O, L, S, C の順に並んでいなければならない。

-dname オプションで指定する場合、空白スペースを含めたい場合はオプションの値を引用符(")で囲う必要がある。

-dnameを引用符で囲う例
$ keytool ... -dname "CN=Alice, S=Osaka Fu, C=JP"

また、半角カンマは属性を区切る特別な意味を持つので、属性値に半角含めたい場合はバックスラッシュでエスケープする必要がある。

属性値に半角カンマを使用する
$ keytool ... -dname CN=Hoge\,Fuga

証明書の有効期間

証明書の有効期間は、次の2つのオプションで制御できる。

  • -startdate
  • -validity

-startdate は、証明書の有効期間の開始日時になる。
-validity は、証明書の有効期間を日数で指定する。

つまり、 -startdate で指定した日時から -validity で指定した日数の間が、証明書の有効期間になる。

-startdate のデフォルト値は、鍵を作成したときの実行日時で、 -validity のデフォルト値は 90 となっている。
したがって、証明書のデフォルトの有効期間は、鍵ペアを作成した日時から 90 日間ということになる。

-startdate は、相対的な方法と絶対的な方法のいずれかで指定できる。

相対的な方法では、実行日時からの相対的な時間で開始日時を指定する。
具体的には次のように記述する。

相対的な開始日時の指定
$ keytool ... -startdate +10y-1m+5d+8H

この指定は、 +10y で「プラス10年」、-1m で「マイナス1ヶ月」、+5d で「プラス5日」、+8H で「プラス8時間」を表している。

相対指定の書式は ([+-]nnn[ymdHMS])+ となっている。
つまり、年・月・日・時・分・秒をそれぞれプラス/マイナスでどれくらいシフトさせるかで指定する。

一方、絶対指定は次のように指定する。

絶対的な開始日時の指定
$ keytool ... -startdate "2019/12/01 12:00:00"

こちらは見たまんま。

書式としては、 [yyyy/mm/dd] [HH:MM:SS] となる。
省略した方は実行日時が利用される。

つまり、年月日だけを指定した場合は、時分秒が実行時間になり、
時分秒だけを指定した場合は、年月日が実行日になる。

証明書を出力する

$ keytool -exportcert -alias alice-key -file alice-cert
証明書がファイル<alice-cert>に保存されました

-exportcert コマンドを使うと、指定したエントリにある証明書を X.509 形式で出力できる。
(秘密鍵のエントリは証明書がないので指定できない)

デフォルトは標準出力に出力される。
ファイルに出力するには -file オプションを指定する。

出力形式は、デフォルトはバイナリ形式となる。
PEM 形式にするには、 -rfc オプションを指定する。

PEM形式で出力する例
$ keytool -exportcert -alias alice-key -rfc
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIEFj7+/DANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJj
...略...
7ticcWecmUspMZ2dx/lO
-----END CERTIFICATE-----

証明書ファイルの内容を表示する

$ keytool -printcert -file alice-cert
所有者: CN=alice
発行者: CN=ca
シリアル番号: 163efefc
有効期間の開始日: Sat Apr 13 11:45:48 JST 2019終了日: Fri Jul 12 11:45:48 JST 2019
証明書のフィンガプリント:
         SHA1: 07:E5:76:25:83:D1:C2:C5:66:C3:87:9E:E9:7A:B0:E8:1C:07:63:83
         SHA256: 55:ED:F8:4B:CC:3F:C9:A9:3F:5D:D8:B3:CC:51:33:58:3E:36:4A:10:06:1F:E7:19:94:25:73:66:30:E7:85:EF
署名アルゴリズム名: SHA256withRSA
サブジェクト公開キー・アルゴリズム: 2048ビットRSAキー
バージョン: 3

拡張:
...

-printcert コマンドを -file オプションを指定して実行すると、指定した証明書ファイルの中身を確認できる。
-file を指定しない場合は、デフォルトで標準入力から情報を読み取る。

デフォルトは、上述のように人間が読みやすい形式で出力される。
PEM 形式で出力するには -rfc オプションを指定する。

PEM形式で出力した場合
$ keytool -printcert -file alice-cert -rfc
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIEFj7+/DANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJj
...略...
7ticcWecmUspMZ2dx/lO
-----END CERTIFICATE-----

-printcert コマンドはキーストアに関係ないので、 -keystore を指定せずに実行できる。

任意の SSL サーバーの証明書を表示する

$ keytool -printcert -sslserver qiita.com
Certificate #0
====================================
所有者: CN=qiita.com
発行者: CN=Amazon, OU=Server CA 1B, O=Amazon, C=US
シリアル番号: 793de44ec0e815de65a3fbb3e35a1e2
有効期間の開始日: Sun Mar 31 09:00:00 JST 2019終了日: Thu Apr 30 21:00:00 JST 2020
証明書のフィンガプリント:
         SHA1: C0:D8:EE:56:3B:9F:68:22:0B:36:F3:9E:2A:D3:69:4E:3C:1D:61:44
         SHA256: 57:28:2C:9D:D0:43:19:08:A4:CC:D3:52:CF:5F:32:16:EC:9D:DA:4A:4E:D1:5C:1F:3A:EB:39:3F:76:A3:91:D4
署名アルゴリズム名: SHA256withRSA
サブジェクト公開キー・アルゴリズム: 2048ビットRSAキー
バージョン: 3

...

Certificate #3
====================================
所有者: CN=Starfield Services Root Certificate Authority - G2, O="Starfield Technologies, Inc.", L=Scottsdale, ST=Arizona, C=US
発行者: OU=Starfield Class 2 Certification Authority, O="Starfield Technologies, Inc.", C=US
シリアル番号: a70e4a4c3482b77f
...

-printcert コマンドに -sslserver オプションをつけて実行すると、指定した SSL サーバーの証明書を出力できる。

-sslserver にはホストとポートを host:port の形で指定できる。
ポート番号は省略可能で、デフォルトは 443 になる。

プロキシ環境下で実行する場合は、 -J-Dhttps.proxyHost-J-Dhttps.proxyPort オプションでプロキシの情報を指定する(例:-J-Dhttps.proxyHost=proxy.host.name -J-Dhttps.proxyPort=8080)。

こちらもデフォルトは人間が読める形式になる。
PEM 形式にするには、 -rfc オプションを指定する。

証明書ファイルとしてローカルに保存したい場合は、 -rfc オプションをつけたうえで出力結果をファイルにリダイレクトすればいい(-file は入力ファイルのオプションなので、これを指定してもファイルには保存されない)。

証明書署名要求を作成する

$ keytool -certreq -alias alice-key
-----BEGIN NEW CERTIFICATE REQUEST-----
MIICkDCCAXgCAQAwGzELMAkGA1UEBhMCSlAxDDAKBgNVBAMTA1RvbTCCASIwDQYJ
...略...
wX7FTUBXCD8CjXFxosmZ97YJf7Xy89cmdArgVNE9T9pJJpht
-----END NEW CERTIFICATE REQUEST-----

-certreq コマンドを使うと、証明書署名要求を作成できる。

-alias オプションには、対象となる公開鍵エントリのエイリアスを指定する。

デフォルトは標準出力に出力される。
ファイルに出力したい場合は、リダイレクトするか -file オプションで出力先のファイルを指定する。

証明書署名要求をファイルに出力する
$ keytool -certreq -alias alice-key -file alice-certreq

証明書署名要求から証明書を生成する

# キーストアの内容を確認
$ keytool -list
...

キーストアには2エントリが含まれます

ca-key,2019/04/13, PrivateKeyEntry,
...
alice-key,2019/04/13, PrivateKeyEntry,
...

# 証明書署名要求ファイルから証明書を生成
$ keytool -gencert -infile alice-certreq -alias ca-key -outfile alice-cert

-gencert コマンドを使うと、証明書署名要求ファイルから証明書(X.509 形式)を生成できる。

上の例は、証明書署名要求ファイル alice-certreq に対して、キーストア内に存在する鍵ペアエントリ ca-key を使って証明書を生成している。

-infile は、処理対象となる証明書署名要求ファイルを指定する。
未指定の場合は標準入力から入力する。

-alias では、証明書の署名に使用する鍵のエントリを指定する。

-outfile は、結果の証明書を出力するファイルを指定している。
未指定の場合は標準出力に出力される。

デフォルトでは、証明書はバイナリ形式で出力される。
-rfc オプションを指定すると、 PEM 形式で出力できる。

-rfcオプションを指定した場合
$ keytool -gencert -infile alice-certreq -alias ca-key -rfc
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIEF+xeBDANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJj
...略...
es0hcACS0jLw0VoJYjSB
-----END CERTIFICATE-----

証明書を取り込む

証明書を取り込むには -importcert コマンドを使用する。

インポートできるデータの形式は、 PKCS#7 形式の証明書チェーンか X.509 証明書の単独またはシーケンスになる。

このコマンドには2つの機能が存在する。

  1. 証明書応答のインポート
  2. 信頼できる証明書のインポート

-importcert コマンドでは、条件によってこのいずれかの機能が実行される。

次の条件を満たした場合は、証明書応答のインポートとして動作する。

  • -alias で指定した名前のエントリがすでにキーストアに存在する
  • そのエントリがプライベート鍵のエントリである

一方、次の条件を満たした場合は、信頼できる証明書のインポートとして動作する。

  • -alias で指定した名前のエントリがキーストアに存在しない

証明書応答のインポート

-alias で指定した名前のエントリがプライベート鍵のエントリとして既にキーストアに存在する場合、インポートしようとしている証明書は証明書応答と判断される。
つまり、その鍵エントリ内の公開鍵証明書から生成した証明書署名要求に対して、 CA が署名して返してきた証明書応答をインポートしようとしている、という扱いになる。

したがって、 keytool は鍵エントリに含まれる公開鍵証明書を、インポート対象に指定した証明書応答で置き換えるように動作する。

証明書応答の検証

インポートが実際に行われる前に、証明書応答が正当なものか検証が行われる。

検証は、大きく次の2つが行われる。

  1. 証明書応答が、確かに -alias で指定された鍵エントリに対応するものかどうか
  2. 証明書応答が信頼できるかどうか

証明書応答が -alias で指定した鍵エントリに対応するものかどうかは、証明書応答の中に鍵エントリ内の公開鍵と同じものがあるかどうかで判断される。
証明書応答の中に鍵エントリ内の公開鍵証明書が存在しないのであれば、その証明書応答は全然関係のないモノなので、エラーとなる。

jca.jpg

証明書応答が信頼できるかどうかの判定方法は、証明書応答内に存在する証明書の数によって大きく2つに分かれる。

証明書応答内に複数の証明書が含まれる場合

証明書応答内に複数の証明書が含まれる場合は、それらの証明書で有効な証明書チェーンが構築できるかどうかが試される。

まず最初に、証明書応答の中からインポート先の鍵エントリと同じ公開鍵を持つ証明書を見つけ出す。
そして、その証明書を先頭にして、署名の検証が OK となる証明書を次々と繋げていく。

jca.jpg

末尾にきた証明書が自己署名証明書だった場合、その証明書と同じ証明書がキーストア内に存在するかが確認される。
キーストア内に同じ自己署名証明書が存在した場合は、証明書チェーンは正当なものと判断され、構築された証明書チェーンが鍵エントリにインポートされる。
同じ自己署名証明書が存在しなかった場合は、インポートを続行して良いか keytool が確認を求めてくる。

一方、末尾にきた証明書が自己署名証明書でなかった場合、その署名の検証が通る証明書がキーストア内に存在しないか確認される。
キーストア内に該当する証明書が見つかった場合は、証明書チェーンの末尾にキーストア内で見つかった証明書を追加したうえで、鍵エントリに証明書チェーンがインポートされる。
該当する証明書が見つからなかった場合は、インポートを続行して良いか keytool が確認を求めてくる。

jca.jpg

証明書応答が単独の証明書の場合

証明書応答内に証明書が1つだけ存在する場合は、検証の動作が少し変わる。

この場合、 keytool はインポート先のキーストア内に存在する証明書を使って証明書チェーンが再現できないか試みる。

jca.jpg

最終的にルートまで証明書チェーンが再現できた場合は、信頼できるモノとして再現されたチェーンが鍵エントリに登録される。
チェーンが再現できなかった場合(途中で検証OKとなる公開鍵を見つけられなかった場合)、インポートを続行して良いか keytool が確認を求めてくる。

信頼できる証明書のインポート

-alias で指定したエントリがキーストア内に存在しない場合、信頼できる証明書としてインポートが行われる。

この場合、インポートしようとしているデータに証明書が複数存在しても、先頭の1つだけが取り込まれる。

このときも、証明書が正当なものか検証が行われる。
この検証は、証明書応答が単独の証明書だった場合と同じ動作になる。

つまり、キーストア内の証明書を使って証明書チェーンが再現できるかどうかで、正当な証明書かどうかが判定される。

チェーンが再現できればそのままインポートが完了する。
再現できない場合は、インポートを続行して良いか keytool が確認を求めてくる。

cacerts の証明書も検証に利用する

-importcert コマンドのオプションで -trustcacerts を指定すると、検証のときに cacerts 内のルート証明書も利用されるようになる。
(デフォルトは、対象のキーストア内の証明書だけが利用される)

実際にやってみる

# Alice のキーストアを確認
$ keytool -keystore alice.keystore -list
...
キーストアには1エントリが含まれます

alice-key,2019/04/18, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): C7:55:BB:57:C4:3F:57:02:BE:2E:57:1E:7C:6F:F4:A9:55:E5:90:92:6B:7D:DF:5F:14:FF:F2:65:16:81:E1:56

# CA のキーストアを確認
$ keytool -keystore ca.keystore -list
...
キーストアには2エントリが含まれます

middle-ca-key,2019/04/18, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): CE:79:D5:32:D0:1E:5E:3A:55:BB:3E:CD:CE:5E:5F:9F:4D:79:97:2E:E7:EF:F9:60:62:C5:D0:4E:AD:28:B4:60
root-ca-key,2019/04/18, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): C0:06:6C:5C:2C:F5:3D:8B:DB:D4:EC:19:C5:30:A0:90:71:00:CC:17:40:E8:EB:38:64:5B:8F:40:3A:D4:3F:7C

検証のために、2つのキーストアを用意した。

1つは Alice のキーストアで、現在は自作した自己署名証明書の公開鍵ペアのエントリが1つだけ存在している(alice-key)。

もう1つは CA (認証局)のキーストアで、中間認証局用の公開鍵ペア(middle-ca-key)とルート認証局用の公開鍵ペア(root-ca-key)の2つのエントリが存在している。
(ちなみに、 middle-ca-keyroot-ca-key で署名済み)

ここから、 Alice の公開鍵ペアから証明書署名要求を生成し、認証局の鍵で署名し、 Alice のキーストアにインポートしてみる。

# Alice の鍵から証明書署名要求を生成
$ keytool -keystore alice.keystore -certreq -alias alice-key -file alice.csr

# 中間認証局の鍵で署名
$ keytool -keystore ca.keystore -gencert -alias middle-ca-key -infile alice.csr -outfile alice.cer

# 生成された証明書の中身を確認
$ keytool -printcert -file alice.cer
証明書[1]:
所有者: CN=alice
発行者: CN=middle-ca
シリアル番号: 25fc2e01
...

証明書[2]:
所有者: CN=middle-ca
発行者: CN=root-ca
シリアル番号: 248f2111
...

Alice の鍵ペアから証明書署名要求を生成し、中間認証局の鍵で署名した公開鍵証明書を生成した。

中身をみると、 Alice→中間認証局の順に証明書がチェーンしていることがわかる(ルート認証局の証明書は入っていない)。

これを、 Alice の元の鍵エントリにインポートする(つまり、証明書応答としてインポートする)。

$ keytool -keystore alice.keystore -importcert -file alice.cer -alias alice-key

応答したトップレベルの証明書:

所有者: CN=middle-ca
発行者: CN=root-ca
シリアル番号: 248f2111
...

... は信頼されていません。 応答をインストールしますか。[いいえ]:

トップレベルの証明書が信頼できないと判断され、インポートして良いか確認を促された。
root-ca の証明書は Alice のキーストアには存在しないので、インポートしようとした証明書応答は信頼できないモノと判断されている。

ここは一旦インポートを中断し、先にルート認証局の証明書を Alice のキーストアにインポートする。

# ルート認証局の公開鍵証明書をエクスポート
$ keytool -keystore ca.keystore -exportcert -alias root-ca-key -file root-ca.cer
証明書がファイル<root-ca.cer>に保存されました

# Alice のキーストアにルート認証局の証明書をインポート
$ keytool -keystore alice.keystore -importcert -file root-ca.cer -alias root-ca-cert
所有者: CN=root-ca
発行者: CN=root-ca
シリアル番号: 13b64cb6
...
証明書のフィンガプリント:
         SHA1: E9:D2:EF:D2:1D:86:8F:04:50:0C:ED:DD:5B:C1:C5:DD:FE:64:77:3D
         SHA256: C0:06:6C:5C:2C:F5:3D:8B:DB:D4:EC:19:C5:30:A0:90:71:00:CC:17:40:E8:EB:38:64:5B:8F:40:3A:D4:3F:7C
...

この証明書を信頼しますか。 [いいえ]:  y
証明書がキーストアに追加されました

-alias に指定したエントリは存在しないので、 -importcert コマンドは「信頼できる証明書のインポート」として動作する。

この証明書は自己署名証明書なので、インポートする人(キーストアの管理者)が信頼するかどうか決めるしか無い。
ここは信頼するので、 y でインポートを完了させた。

実際に信頼できる証明書をインポートする場合は、画面に表示されているフィンガープリントなどの情報を見て、確かに意図した証明書をインポートしていることを注意深く確認しなければならない。

あらためて Alice の証明書応答をインポートしてみる。

# Alice の証明書応答をインポート
$ keytool -keystore alice.keystore -importcert -file alice.cer -alias alice-key
証明書応答がキーストアにインストールされました

# インポート後の Alice の鍵エントリを確認
$ keytool -keystore alice.keystore -storepass password -list -v -alias alice-key
別名: alice-key
作成日: 2019/04/18
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 3
証明書[1]:
所有者: CN=alice
発行者: CN=middle-ca
シリアル番号: 25fc2e01
...

証明書[2]:
所有者: CN=middle-ca
発行者: CN=root-ca
シリアル番号: 248f2111
...

証明書[3]:
所有者: CN=root-ca
発行者: CN=root-ca
シリアル番号: 13b64cb6
...

今度はルート認証局の証明書がインストール済みだったため、証明書チェーンの検証がすんなり成功し、そのままインストールが完了した。

また、インポート後は Alice の鍵エントリ内の公開鍵証明書に証明書チェーンが格納されているのがわかる。

KeyStore

keytool コマンドで生成・管理するキーストアファイルは、 Java のプログラムからもアクセスできるように API が用意されている。

KeyStore インスタンスを取得する

jshell
jshell> var keystore = KeyStore.getInstance("PKCS12")
keystore ==> java.security.KeyStore@3fd7a715

キーストアにアクセスするには、 KeyStore クラスを使用する。

getInstance() では、キーストアのタイプを指定する。
Java 9 以降であれば、キーストアのタイプはデフォルトで PKCS12 なので、 "PKCS12" でインスタンスを生成している。
(古いキーストアであれば JKS の可能性もあるので、その場合は "JKS" と指定する)

キーストアファイルを読み込む

jshell
jshell> char[] password = { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }
password ==> char[8] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }

jshell> keystore.load(new FileInputStream("alice.keystore"), password)

キーストアファイルを読み込むには、 load() メソッドを使う。
第一引数にはキーストアファイルの InputStream を指定し、第二引数にはキーストアファイルのパスワードを char 配列で指定する。

インスタンス取得と同時に読み込む

jshell
jshell> var keystore = KeyStore.getInstance(new File("alice.keystore"), password)
keystore ==> java.security.KeyStore@525b461a

getInstance(File, char[]) なら、 KeyStore インスタンスの生成からキーストアファイルのロードまでを一回で実行できる。

キーストアのタイプは自動判定される。

全エントリのエイリアス名を取得する

jshell
jshell> keystore.aliases().asIterator().forEachRemaining(System.out::println)
alice-key
aes-key
root-ca-cert

aliases() メソッドで、全エントリのエイリアス名を取得できる。

エイリアス名からエントリの種類を確認する

jshell
jshell> keystore.isKeyEntry("alice-key")
$12 ==> true

jshell> keystore.isCertificateEntry("alice-key")
$13 ==> false

jshell> keystore.isKeyEntry("root-ca-cert")
$14 ==> false

jshell> keystore.isCertificateEntry("root-ca-cert")
$15 ==> true

エイリアスが指すエントリが鍵エントリかどうかは isKeyEntry() で確認できる。

信頼できる証明書エントリかどうかは、 isCertificateEntry() で確認できる。

秘密鍵を取得する

jshell
jshell> var seckey = keystore.getKey("seckey", password)
seckey ==> javax.crypto.spec.SecretKeySpec@fffe8f5b

jshell> seckey.getAlgorithm()
$29 ==> "AES"

getKey() で、指定したエイリアスの鍵を秘密鍵として取得できる。

第一引数はエイリアス名を渡し、第二引数は鍵を保護しているパスワード(普通はキーストアのパスワードと同じ)を渡す。

プライベート鍵を取得する

jshell
jshell> var alicePrivateKey = keystore.getKey("alice-key", password)
alicePrivateKey ==> SunRsaSign RSA private CRT key, 2048 bits
  param ... 96220784207006016968977089

jshell> alicePrivateKey.getClass()
$38 ==> class sun.security.rsa.RSAPrivateCrtKeyImpl

公開鍵ペアの鍵エントリに対して getKey() を使うと、プライベート鍵を取得できる。

公開鍵(証明書)を取得する

jshell
// 証明書を取得
jshell> Certificate aliceCert = keystore.getCertificate("alice-key")
aliceCert ==> [
[
  Version: V3
  Subject: CN=alice
  Signature ... 3 D5  .........%$.m...

]

// 証明書から公開鍵を取得
jshell> PublicKey alicePublicKey = aliceCert.getPublicKey()
alicePublicKey ==> Sun RSA public key, 2048 bits
  params: null
  mo ... 9
  public exponent: 65537

公開鍵ペアの鍵エントリに対して getCertificate() を使うと、公開鍵証明書(java.security.cert.Certificate)を取得できる。

証明書チェーンを取得する

jshell
jshell> Certificate[] chain = keystore.getCertificateChain("alice-key")
chain ==> Certificate[3] { [
[
  Version: V3
  Subject: CN= ... C0  .;;.O...&.....N.

] }

getCertificateChain() を使うと、指定したエントリに存在する証明書チェーンを Certificate の配列で取得できる。

秘密鍵エントリを追加する

jshell
// 秘密鍵を生成
jshell> var secKey = KeyGenerator.getInstance("AES").generateKey()
secKey ==> javax.crypto.spec.SecretKeySpec@fffe806b

// 秘密鍵のエントリを生成
jshell> var secKeyEntry = new KeyStore.SecretKeyEntry(secKey)
secKeyEntry ==> Secret key entry with algorithm AES

// 秘密鍵エントリをキーストアに登録
jshell> var protection = new KeyStore.PasswordProtection(password)
protection ==> java.security.KeyStore$PasswordProtection@27d415d9

jshell> keystore.setEntry("sec-key", secKeyEntry, protection)

// 登録された秘密鍵エントリを取り出して確認
jshell> keystore.getKey("sec-key", password)
$10 ==> javax.crypto.spec.SecretKeySpec@fffe806b

キーストアにエントリを追加するには、 setEntry() を使う。

第一引数は、エントリのエイリアス。
第二引数は、登録するエントリ。
第三引数は、エントリの保護に関するパラメータを渡す。

秘密鍵のエントリは、 KeyStore.SecretKeyEntry で作成する。

パラメータは、エントリを暗号化するときに使用するパスワードを渡すことになる。
一応キーストアとは別のパスワードを指定可能だが、同じにしておくのが無難?(keytool は、キーストアのタイプが PKCS12 ならキーストアと同じパスワードを使うようになっている)

ファイルに出力する

jshell
jshell> keystore.store(new FileOutputStream("alice.keystore"), password)

store() で、指定した出力ストリームに KeyStore の内容を出力できる。

証明書

新規作成する API はない?

証明書を表すクラスとして java.security.cert.Certificate というクラスが用意されており、このインスタンスを構築するためのクラスとして java.security.cert.CertificateFactory というクラスが存在する。

しかし、 CertificateFactory は既存の証明書を読み込んで Certificate を構築するもので、ゼロから証明書を生成するものではない。

ざっと java.security.cert パッケージ を見ても、証明書を生成するようなクラスは見当たらない。

一応 Keytool の -gencert コマンドの実装を見てみたが、 sun.security.x509 パッケージ(内部API)のクラスが使われていた。

標準 API だけで証明書を生成する方法はなさげ。
keytool を使って生成するしかなさそう。

証明書を読み込む

指定可能な証明書タイプ

エンコードされた証明書を読み込むには、 CertificateFactory を使用する。

getInstance() で指定できる証明書のタイプは、標準機能としては X.509 だけで、 Javadoc なども基本的に X.509 の説明が中心となって説明されている。

CertificateFactory型 | Javaセキュリティ標準アルゴリズム名

ということで、ここでの説明も X.509 を前提として記述する。

証明書を1つずつ読み込む

jshell
// CertificateFactory を生成
jshell> var factory = CertificateFactory.getInstance("X.509")
factory ==> java.security.cert.CertificateFactory@3abbfa04

// 読み込む証明書ファイル(alice.cer)の入力ストリームを生成
jshell> var in = new FileInputStream("alice.cer")
in ==> java.io.FileInputStream@31ef45e3

// 入力ストリームから証明書の情報を読み込む
jshell> factory.generateCertificate(in)
$5 ==> [
[
  Version: V3
  Subject: CN=alice
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
...
]

jshell> factory.generateCertificate(in)
$6 ==> [
[
  Version: V3
  Subject: CN=middle-ca
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
...
]

jshell> factory.generateCertificate(in)
|  例外java.security.cert.CertificateException: Could not parse certificate: java.io.IOException: Empty input
|        at X509Factory.engineGenerateCertificate (X509Factory.java:110)
|        at CertificateFactory.generateCertificate (CertificateFactory.java:355)
|        at (#7:1)
|  原因: java.io.IOException: Empty input
|        at X509Factory.engineGenerateCertificate (X509Factory.java:106)
|        ...

エンコードされた X.509 証明書を読み込むには、 generateCertificate() を使用する。

対象データは、 DER エンコードされたバイナリ形式のデータか、もしくは PEM 形式のデータである必要がある。

対象データに複数の証明書のデータが含まれている場合、 generateCertificate() を呼び出すごとに先頭から1つずつ証明書を読み込むことができる。
(証明書がもう存在しないのにさらに読み込もうとすると、例外がスローされる)

複数の証明書をまとめて読み込む

jshell
jshell> Collection<? extends Certificate> certs = factory.generateCertificates(new FileInputStream("alice.cer"))
certs ==> [[
[
  Version: V3
  Subject: CN=alice
  Signatur ...  70  ....$..&...2.n.p

]]

jshell> certs.size()
$12 ==> 2

generateCertificates() なら、対象データ内のすべての証明書をまとめて読み込める。

証明書の署名を検証する

jshell
// ↑で読み込んだ証明書のコレクションから、1つずつ証明書を抽出
jshell> var iterator = certs.iterator()
iterator ==> java.util.ArrayList$Itr@27808f31

// alice の証明書を取得
jshell> var aliceCert = iterator.next()
aliceCert ==> [
[
  Version: V3
  Subject: CN=alice
  Signature ... 3 D5  .........%$.m...

]

// 中間認証局の証明書を取得
jshell> var middleCaCert = iterator.next()
middleCaCert ==> [
[
  Version: V3
  Subject: CN=middle-ca
  Signa ... 0 70  ....$..&...2.n.p

]

// 中間認証局の公開鍵で、 alice の証明書を検証(検証OK)
jshell> aliceCert.verify(middleCaCert.getPublicKey())

// alice 自身の公開鍵で、 alice の証明書を検証(検証NG)
jshell> aliceCert.verify(aliceCert.getPublicKey())
|  例外java.security.SignatureException: Signature does not match.
|        at X509CertImpl.verify (X509CertImpl.java:459)
|        at X509CertImpl.verify (X509CertImpl.java:391)
|        at (#18:1)

証明書の署名を検証するには verify() を使用する。

引数には、検証で使用する公開鍵を渡す。

検証が OK であれば何も起こらず、 NG の場合は例外がスローされる。

乱数

jshell
// SecureRandom インスタンスを生成
jshell> var random = new SecureRandom()
random ==> Hash_DRBG,SHA-256,128,reseed_only

// 100 未満の乱数を取得
jshell> random.nextInt(100)
$3 ==> 91

// 512 ビットのランダムなバイト配列を生成
jshell> byte[] bytes = new byte[64]
bytes ==> byte[64] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... , 0, 0, 0, 0, 0, 0, 0, 0 }

jshell> random.nextBytes(bytes)

jshell> bytes
bytes ==> byte[64] { 97, 86, -17, 9, 81, 39, -18, -43, 40, -43, -21, 93, 112, -78, -1, 59, -5, 110, 115, 13, -86, 34, -81, -90, -107, -82, -102, -72, 19, 87, 9, -38, -91, -7, 47, -119, 103, -24, 52, -40, -3, 36, -9, -62, 10, -32, 36, 27, -74, -75, 58, -54, -124, -49, -6, 77, 78, 32, 96, 66, 67, -86, -39, -98 }

暗号用の乱数生成には、 SecureRandom を使用する。

Java には乱数を生成するためのクラスとして Random クラスも存在する。
しかし、こちらは予測不可能性を持たないため、暗号技術には利用してはいけない。

SecureRandom は、他のエンジンクラスとは異なり getInstace() 以外にも コンストラクタ でインスタンスを生成できる。

コンストラクタでインスタンスを生成した場合、優先度の高いプロバイダからアルゴリズムが選択される。

環境ごとにどのアルゴリズムが選択されるかは、 SecureRandom実装 | 4 JDKプロバイダ・ドキュメント で確認できる。

getInstance() を使うべきか、コンストラクタを使うべきか

SecureRandom のインスタンスは、他のエンジンクラス同様 getInstance() で取得することもできるし、コンストラクタで取得することもできる。
前者はアルゴリズムを明示する必要があるが、後者はあらかじめ決められた優先順位によってアルゴリズムが決定する。

どちらを使って SecureRandom のインスタンスは生成すべきなのか?

Javadoc や JCA の公式ドキュメントを見ても、明確に「こちらを使え」と書かれているところは見つけられなかった。

いろいろ調べて出した個人的な結論は、「特に理由が無いのならコンストラクタを使う」となった(あくまで個人的な結論なので、間違っているかも)。

理由は、コンストラクタを使用した方が、環境ごとに最適なアルゴリズムを選択しつつ可搬性を維持することができるから。

Unix 系の環境では、デフォルトのアルゴリズムは NativePRNG になる。
しかし、このアルゴリズムは Windows 環境では使用できない。
つまり、アルゴリズムを NativePRNG 指定にしていると、そのプログラムは Windows には移植できないことになる。
(逆に、 Windows-PRNG は Unix 系環境では使用できない)

また、 Windows 環境では Java 8 までデフォルトのアルゴリズムは SHA1PRNG だった。
しかし、 Java 9 で DRBG が追加されたことで、最優先は DRBG に置き換わった(JEP 273: DRBG-Based SecureRandom Implementations)。

DRBGNIST Special Publication 800-90A Revision 1 で説明されている疑似乱数生成方法で、 SHA1PRNG よりも強力らしい。

このようにアルゴリズムが新しいものに置き換わるといったケースにも対応しやすいので、コンストラクタを使ったほうが良いのかなと思う。
(あくまで、個人的な結論です)

getInstanceStrong()

Java 8 で getInstanceStrong() というメソッドが追加されている。

名前に Strong とあるように、強力な乱数を生成するアルゴリズムが選択される。
公開鍵暗号の鍵ペア生成など、重要な値の生成のときに利用するらしい。

例えば Unix 系環境で使用すると、具体的なアルゴリズムは NativePRNGBlocking になる。

デフォルトで選択される NativePRNG と比べると何が違うのかというと、 nextBytes()generateSeed() が、それぞれ /dev/random/dev/urandom のどちらを使用するかが異なる。

アルゴリズム nextBytes() generateSeed()
NativePRNG /dev/urandom /dev/random
NativePRNGBlocking /dev/random /dev/random
NativePRNGNonBlocking /dev/urandom /dev/urandom

SecureRandom | 表4-4 SUNプロバイダでのアルゴリズム

この /dev/random/dev/urandom は乱数を生成するための疑似デバイスで、 Unix 系の OS で利用できる。

/dev/random は、環境ノイズを収集して乱数を生成するため、真の乱数を得ることができる。
しかし、情報が十分に集まっていない状態で乱数を得ようとすると、処理がブロック(待機)させられてしまう。

一方、 /dev/urandom は収集した情報を使い回すことで、処理をブロックすることなく乱数を生成できる。
しかし、情報を再利用しているため乱数としての安全性は /dev/random に劣る。

参考

/dev/random は、安全性は高いが、代わりにプログラムの性能を低下させる恐れがある。
/dev/urandom は、安全性は劣るが、代わりにプログラムの性能は落とさずに済む。

デフォルトで選択される NativePRNG は、シードの生成には真の乱数を得られる /dev/random を使用するが、通常の乱数生成では /dev/urandom を使用している。
つまり、 NativePRNG は安全性をやや落としつつも、性能は確保できるようになっている。

一方 NativePRNGBlocking は、常に /dev/random から乱数を得るようになっている。
つまり、性能は捨てて安全性に全振りしている。

プログラムの性能よりも安全性を優先しなければならない場面では getInstanceStrong() を使い、どちらも両立しなければならない場面ではコンストラクタでデフォルト実装を使うようにすればいいのだと思う(個人の意見)。

Diffie-Hellman 鍵交換

Diffie-Hellman 鍵交換を実現するためのクラスとして、 KeyAgreement が用意されている。

KeyAgreement を用いた鍵交換の流れは、以下のような感じになる。

jca.png

以下、 Alice を表す jshell と Bob を表す jshell をそれぞれ立ち上げて、鍵の交換を試してみる(カレントディレクトリは2つとも同じ場所)。

Alice
// 鍵ペアを生成する
jshell> var aliceKeyPairGen = KeyPairGenerator.getInstance("DH")
aliceKeyPairGen ==> java.security.KeyPairGenerator$Delegate@6d7b4f4c

// 鍵ペアを生成
jshell> var aliceKeyPair = aliceKeyPairGen.generateKeyPair()
aliceKeyPair ==> java.security.KeyPair@1786dec2

// Alice の KeyAgreement を生成
jshell> var aliceKeyAgree = KeyAgreement.getInstance("DH")
aliceKeyAgree ==> javax.crypto.KeyAgreement@2c039ac6

// Alice の秘密鍵で初期化
jshell> aliceKeyAgree.init(aliceKeyPair.getPrivate())

// Alice の公開鍵をファイルに出力
jshell> writeFile("alice-dh-public-key", aliceKeyPair.getPublic().getEncoded())

// 公開鍵のフォーマットは X.509
jshell> aliceKeyPair.getPublic().getFormat()
$32 ==> "X.509"

鍵交換には、まずは KeyPairGenerator を使って鍵ペアを生成する。
アルゴリズム名には DiffieHellman か、略称の DH を指定する。

この鍵ペアの公開鍵には、素数 $p$ と原始元 $g$、そして適当に選択された乱数 $a$ から計算した値 $y = g^{a} \bmod p$ が含まれている。

KeyAgreementDiffieHellman (または DH)でインスタンスを取得し、秘密鍵で初期化しておく。

Bob
// Alice の公開鍵を読み取り
jshell> var encodedAlicePublicKey = readFile("alice-dh-public-key")
encodedAlicePublicKey ==> byte[556] { 48, -126, 2, 40, 48, -126, 1, 27, 6,  ...  70, 34, 8, -3, -119, 14 }

// Alice のエンコードされた公開鍵を復元
jshell> var bobKeyFactory = KeyFactory.getInstance("DH")
bobKeyFactory ==> java.security.KeyFactory@7a9273a8

jshell> var alicePublicKeySpec = new X509EncodedKeySpec(encodedAlicePublicKey)
alicePublicKeySpec ==> java.security.spec.X509EncodedKeySpec@e874448

jshell> var alicePublicKey = bobKeyFactory.generatePublic(alicePublicKeySpec)
alicePublicKey ==> SunJCE Diffie-Hellman Public Key:
y:
    17cc14 ...
g:
    02
l:
    1024

Alice から受け取った公開鍵は X.509 形式なので、 X509EncodedKeySpec を使って復元できる。

KeySpec から Key を生成するのは KeyFactory の役割なので、 KeyFactory を使って復元を行う。
アルゴリズム名は、 DiffieHellman または DH を指定する。

Bob
// Bob の鍵ペアを生成
jshell> var bobKeyPairGen = KeyPairGenerator.getInstance("DH")
bobKeyPairGen ==> java.security.KeyPairGenerator$Delegate@42d8062c

jshell> var dhParams = ((DHPublicKey)alicePublicKey).getParams()
dhParams ==> javax.crypto.spec.DHParameterSpec@cb51256

jshell> bobKeyPairGen.initialize(dhParams)

jshell> var bobKeyPair = bobKeyPairGen.generateKeyPair()
bobKeyPair ==> java.security.KeyPair@5bcea91b

復元された Alice の公開鍵は、 DHPublicKey という型になっている。
この getPrams() メソッドを使って、 Alice 側で生成された鍵交換の具体的なパラメータを取得する。

鍵交換のパラメータが取得できたら、その値から Bob 側の鍵ペアを生成する。

Bob
// Bob の KeyAgreement を生成して、 Bob の秘密鍵で初期化
jshell> var bobKeyAgree = KeyAgreement.getInstance("DH")
bobKeyAgree ==> javax.crypto.KeyAgreement@27f723

jshell> bobKeyAgree.init(bobKeyPair.getPrivate())

// Bob の公開鍵をファイルに出力
jshell> writeFile("bob-dh-public-key", bobKeyPair.getPublic().getEncoded())

Bob 側の KeyAgreement は、 Bob の秘密鍵で初期化しておく。

そして、 Bob の公開鍵を Alice に送る。

Alice
// Bob の公開鍵を読み取り
jshell> var encodedBobPublicKey = readFile("bob-dh-public-key")
encodedBobPublicKey ==> byte[557] { 48, -126, 2, 41, 48, -126, 1, 27, 6,  ... 5, -20, 52, -85, -95, 51 }

// Alice 側で Bob の公開鍵を復元
jshell> var aliceKeyFactory = KeyFactory.getInstance("DH")
aliceKeyFactory ==> java.security.KeyFactory@7b69c6ba

jshell> var bobPublicKeySpec = new X509EncodedKeySpec(encodedBobPublicKey)
bobPublicKeySpec ==> java.security.spec.X509EncodedKeySpec@12f41634

jshell> var bobPublicKey = aliceKeyFactory.generatePublic(bobPublicKeySpec)
bobPublicKey ==> SunJCE Diffie-Hellman Public Key:
y:
    f82361 ...
g:
    02
l:
    1024

Alice は、 Bob から受け取った公開鍵を復元する。
この手順は、 Bob が Alice の公開鍵を復元したときと同じ。

Alice
// Bob の公開鍵の情報をもとに、秘密鍵を生成
jshell> aliceKeyAgree.doPhase(bobPublicKey, true)
$20 ==> null

jshell> var secret = aliceKeyAgree.generateSecret()
secret ==> byte[256] { 102, 50, -83, 94, 119, -90, -46, -27, ... 66, -94, -121, -19, -119 }
Bob
// Alice の公開鍵の情報をもとに、秘密鍵を生成
jshell> bobKeyAgree.doPhase(alicePublicKey, true)
$25 ==> null

jshell> var secret = bobKeyAgree.generateSecret()
secret ==> byte[256] { 102, 50, -83, 94, 119, -90, -46, -27, ... 66, -94, -121, -19, -119 }

最後に、それぞれ KeyAgreementdoPhase() メソッドを呼ぶことで、秘密鍵生成の準備が完了する。

秘密鍵の生成は、 generateSecret() で行う。

以下、生成された秘密鍵を使って暗号化と復号ができることを確認している。

Alice
// 生成された鍵の先頭 128 ビットを使って AES の秘密鍵を生成
jshell> var secretKey = new SecretKeySpec(secret, 0, 16, "AES")
secretKey ==> javax.crypto.spec.SecretKeySpec@fffe86a4

// AES で暗号化
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5PAdding")
cipher ==> javax.crypto.Cipher@12cdcf4

jshell> cipher.init(Cipher.ENCRYPT_MODE, secretKey)

// 暗号文を書き出し
jshell> writeFile("cryptograph", cipher.doFinal("Hello Diffie-Hellman!!".getBytes()))

// 初期化ベクトルを書き出し
jshell> writeFile("iv", cipher.getParameters().getEncoded())
Bob
// Alice 同様、 AES 用の秘密鍵を生成
jshell> var secretKey = new SecretKeySpec(secret, 0, 16, "AES")
secretKey ==> javax.crypto.spec.SecretKeySpec@fffe86a4

// 初期化ベクトルをファイルから復元
jshell> var params = AlgorithmParameters.getInstance("AES")
params ==>

jshell> params.init(readFile("iv"))

// Cipher を復号モードで準備
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher ==> javax.crypto.Cipher@cb51256

jshell> cipher.init(Cipher.DECRYPT_MODE, secretKey, params)

// 暗号文を復号
jshell> var message = cipher.doFinal(readFile("cryptograph"))
message ==> byte[22] { 72, 101, 108, 108, 111, 32, 68, 105, 1 ... 08, 109, 97, 110, 33, 33 }

jshell> new String(message)
$31 ==> "Hello Diffie-Hellman!!"

参考


  1. CipherInputStreamclose() してなくても結果が中途半端になっていないが、実装をみると入力ストリームが末尾まで達すると doFinal() しているっぽい 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36