JEP 470とは? OpenJDKを調べてみた
JEP 470: PEM Encodings of Cryptographic Objects (Preview)は、セキュリティ・ライブラリとしてPreview版ですが使用できるようになりました。
セキュリティ・ライブラリとしてはもう一つ、JEP 510: Key Derivation Function APIが追加されました。
今回はJEP 470: PEM Encodings of Cryptographic Objects (Preview)を調べてみます。
Summary
暗号鍵、証明書、証明書失効リストを表すオブジェクトを、広く使用されているプライバシー強化メール(PEM)転送形式にエンコードし、その形式からオブジェクトへデコードするためのAPIを導入します。
これはプレビューAPIです。
Goals
- 使いやすさ — PEMテキストと、鍵、証明書、証明書失効リストを表すオブジェクト間の変換を行う簡潔なAPIを定義する。
- 標準サポート — PEMテキストと、バイナリ形式PKCS#8(秘密鍵用)、X.509(公開鍵、証明書、証明書失効リスト用)、PKCS#8 v2.0(暗号化された秘密鍵および非対称鍵用)で標準表現を持つ暗号オブジェクト間の変換をサポートする。
Motivation
JavaプラットフォームAPIは、公開鍵、秘密鍵、証明書、証明書失効リストなどの暗号オブジェクトを豊富にサポートしています。
開発者はこれらのオブジェクトを使用して署名の署名と検証、TLSで保護されたネットワーク接続の検証、その他の暗号操作を実行します。
アプリケーションは、ユーザーインターフェース経由、ネットワーク経由、ストレージデバイスとの間で、暗号オブジェクトの表現を送受信することがよくあります。
この目的には、RFC 7468で定義されたプライバシー強化メール(PEM)形式がよく使用されます。
このテキスト形式は元々、暗号オブジェクトを電子メールで送信するために設計されましたが、時を経て他の目的にも使用・拡張されてきました。
認証局はPEM形式で証明書チェーンを発行します。
OpenSSLなどの暗号ライブラリは、PEMエンコードされた暗号オブジェクトの生成および変換操作を提供します。
OpenSSHなどのセキュリティ重視アプリケーションは通信キーをPEM形式で保存します。
Yubikeyなどのハードウェア認証デバイスはPEMエンコードされた暗号オブジェクトを取り込み、発行します。
以下はPEM形式でエンコードされた暗号オブジェクトの例です。この場合は楕円曲線公開鍵です:
—–BEGIN PUBLIC KEY—–
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u
cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==
—–END PUBLIC KEY—–
PEMテキストは、ヘッダーとフッターに囲まれた鍵のバイナリ表現のBase64エンコードされた表現を含みます。
ヘッダーとフッターにはそれぞれ「BEGIN」と「END」という単語が含まれます。
ヘッダーとフッターの残りのテキストは暗号オブジェクトのタイプ(この場合は公開鍵)を識別します。
鍵の詳細(アルゴリズムや内容など)は、Base64エンコードされたバイナリ表現を解析することで取得できます。
Javaプラットフォームには、PEM形式のテキストをデコードおよびエンコードするための使いやすいAPIが含まれていません。
この課題は、2022年4月のJava暗号化拡張機能調査で確認されました。
各暗号化オブジェクトはバイナリエンコードされた表現を返すメソッドを提供し、Base64 APIを使用してテキストに変換することは可能ですが、残りの作業は開発者に委ねられています:
- 公開鍵のエンコードは単純だが、手間がかかる。
- PEMエンコードされた鍵のデコードには、ソースPEMテキストの注意深い解析、鍵オブジェクト生成に使用するファクトリーの決定、鍵のアルゴリズムの特定が必要である。
- 秘密鍵の暗号化と復号には、十数行以上のコードを要する。
きっと、もっとうまくやれるはずだ。
Description
java.securityパッケージに新しいインターフェースと3つの新しいクラスを導入します:
- DEREncodableインターフェースは、バイナリエンコード可能な鍵または証明書素材を持つ暗号オブジェクトを表すJavaプラットフォームAPIクラスによって実装される。
- PEMEncoderクラスとPEMDecoderクラスは、PEM形式へのエンコードおよびPEM形式からのデコードを目的としています。これらのクラスのインスタンスは不変かつ再利用可能であり、つまり、以前にエンコードまたはデコードされた暗号オブジェクトからの情報を保持しません。
- PEMRecordクラスは、DEREncodableを実装し、JavaプラットフォームAPIが存在しない暗号オブジェクトを表すPEMテキストの符号化と復号化を行うためのものです。
これはプレビューAPIであり、デフォルトでは無効化されています
JDK 25でこのAPIを使用するには、プレビューAPIを有効にする必要があります:
- javac –release 25 –enable-preview Main.java でプログラムをコンパイルし、java –enable-preview Main で実行してください。または、
- ソースコードランチャーを使用する場合、java –enable-preview Main.java でプログラムを実行してください。または、
- jshellを使用する際は、jshell –enable-previewで起動してください。
DER符号化可能暗号オブジェクト
PEMはバイナリデータ用のテキスト形式です。
暗号オブジェクトをPEMテキストにエンコードしたり、PEMテキストを暗号オブジェクトにデコードしたりするには、そのようなオブジェクトをバイナリデータと相互変換する手段が必要です。
幸いなことに、暗号鍵、証明書、証明書失効リスト(CRL)用のJava APIはすべて、そのインスタンスをDER(Distinguished Encoding Rules)形式のバイト配列と相互変換する手段を提供しています。
残念ながら、これらのAPIは階層的に関連しておらず、変換機能を公開する方式も統一されていません。
したがって、我々は新たなインターフェースであるDEREncodableを導入する。
これは、このような変換を提供する暗号APIを識別し、そのインスタンスがPEM形式へのエンコードおよびPEM形式からのデコードが可能であることを示すものである。
この空インターフェースはシールされています。
許可されるクラスおよびインターフェースは、AsymmetricKey、X509Certificate、X509CRL、KeyPair、EncryptedPrivateKeyInfo、PKCS8EncodedKeySpec、X509EncodedKeySpec、およびPEMRecordです:
|
1 2 3 4 5 6 |
public sealed interface DEREncodable permits AsymmetricKey, KeyPair, PKCS8EncodedKeySpec, X509EncodedKeySpec, EncryptedPrivateKeyInfo, X509Certificate, X509CRL, PEMRecord { } |
許可されているクラスおよびインターフェースの一部について、対応する調整を行います:
|
1 2 3 4 5 6 |
public non-sealed interface AsymmetricKey { ... } public non-sealed class PKCS8EncodedKeySpec { ... } public non-sealed class X509EncodedKeySpec { ... } public non-sealed class EncryptedPrivateKeyInfo { ... } public non-sealed abstract class X509Certificate { ... } public non-sealed abstract class X509CRL { ... } |
エンコーディング
PEMEncoderクラスは、DEREncodableオブジェクトをPEMテキストにエンコードするためのメソッドを宣言します:
|
1 2 3 4 5 6 7 8 9 10 |
public final class PEMEncoder { public static PEMEncoder of(); public byte[] encode(DEREncodable so); public String encodeToString(DEREncodable so); public PEMEncoder withEncryption(char[] password); } |
DEREncodableオブジェクトをエンコードするには、まずof()を呼び出してPEMEncoderインスタンスを取得します。
返されるインスタンスはスレッドセーフで再利用可能なため、そのエンコードメソッドを繰り返し使用できます。
エンコードには2つの方法があります。1つは、ISO-8859-1文字セットでエンコードされた文字を含むバイト配列でPEMテキストを返す方法です。
例えば、秘密鍵をエンコードするには:
|
1 2 |
PEMencoder pe = PEMEncoder.of(); byte[] pem = pe.encode(privateKey); |
もう一方のエンコード方法はPEMテキストを文字列として返します。
例えば、公開鍵/秘密鍵ペアを文字列にエンコードするには:
|
1 |
String pem = pe.encodeToString(new KeyPair(publicKey, privateKey)); |
秘密鍵をエンコードする場合は、withEncryptionメソッドを使用して暗号化できます。
このメソッドはパスワードを受け取り、そのパスワードで鍵を暗号化するよう設定された新しい不変のPEMEncoderインスタンスを返します:
|
1 |
String pem = pe.withEncryption(password).encodeToString(privateKey); |
このように構成されたPEMEncoderは、PrivateKeyオブジェクトのみをエンコードできます。
デフォルトの暗号化アルゴリズムを使用します。
非デフォルトの暗号化パラメータを使用する場合、または別の暗号化プロバイダーで暗号化するには、EncryptedPrivateKeyInfoオブジェクトを使用してください(以下を参照)。
デコーディング
PEMDecoderクラスは、PEMテキストをDEREncodableオブジェクトにデコードするためのメソッドを宣言します:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public final class PEMDecoder { public static PEMDecoder of(); public DEREncodable decode(String str); public DEREncodable decode(InputStream is) throws IOException; public <S extends DEREncodable> S decode(String string, Class<S> cl); public <S extends DEREncodable> S decode(InputStream is, Class<S> cl) throws IOException; public PEMDecoder withDecryption(char[] password); public PEMDecoder withFactory(Provider provider); } |
PEMテキストをデコードするには、まずof()を呼び出してPEMDecoderインスタンスを取得します。
返されるインスタンスはスレッドセーフで再利用可能なため、そのデコードメソッドを繰り返し使用できます。
デコードには4つのメソッドがあり、それぞれDEREncodableオブジェクトを返します。
instanceof演算子やswitch文を用いたパターンマッチングで、返される暗号オブジェクトの型を識別できます。
例えば、公開鍵または秘密鍵のいずれかをエンコードすると予想されるPEMテキストをデコードするには:
|
1 2 3 4 5 6 |
PEMDecoder pd = PEMDecoder.of(); switch (pd.decode(pem)) { case PublicKey publicKey -> ...; case PrivateKey privateKey -> ...; default -> throw new IllegalArgumentException(...); } |
暗号化されたオブジェクトの型を事前に知っている場合、Class引数を取るデコードメソッドのいずれかに、対応するクラスを渡すことができます。
これにより、メソッドの結果の型に対してパターンマッチングを行う必要や、型をチェックしてキャストする手間を省けます。
例えば、型がECPublicKeyであると分かっている場合:
|
1 |
ECPublicKey key = pd.decode(pem, ECPublicKey.class); |
この場合、クラスが正しくない場合、ClassCastExceptionがスローされます。
入力PEMテキストが秘密鍵をエンコードしている場合、withDecryptionメソッドを使用して復号できます。
このメソッドはパスワードを受け取り、鍵をPrivateKeyオブジェクトに復号するように構成された新しいPEMDecoderインスタンスを返します。
このように構成されたPEMDecoderは、暗号化されていないオブジェクトのデコードも引き続き可能です。
例えば、ECPrivateKeyを復号するには:
|
1 2 |
ECPrivateKey eckey = pd.withDecryption(password) .decode(pem, ECPrivateKey.class); |
秘密鍵をエンコードしたPEMテキストを復号化する際、パスワードを指定しない場合、復号メソッドはEncryptedPrivateKeyInfoインスタンスを返します。
このインスタンスを使用して復号化し、PrivateKeyオブジェクトを生成できます(下記参照)。
状況によっては、PEMテキストをデコードする際に特定の暗号プロバイダーを使用する必要が生じる場合があります。
withFactoryメソッドは、指定されたプロバイダーを使用して暗号オブジェクトを生成する新しいPEMDecoderインスタンスを返します。
例えば、特定のプロバイダーで証明書をデコードするには:
|
1 2 |
PEMDecoder d = pd.withFactory(providerFactory); Certificate c = d.decode(pem, X509Certificate.class); |
プロバイダが要求された種類の暗号オブジェクトを生成できない場合、IllegalArgumentException がスローされます。
PEMテキストを暗号オブジェクトにデコードする際、入力文字列またはバイトストリーム内のPEMヘッダーより前のデータはすべて無視されます。
そのデータが必要な場合は、PEMRecordオブジェクトにデコードすることで取得できます。
PEM入力が解析できない場合、IllegalArgumentExceptionがスローされます。
入力ストリームから読み取られるバイトは、ISO-8859-1文字セットでエンコードされた文字を表すとみなされます。
PEMRecordクラス
PEMRecordクラスはDEREncodableを実装します。
そのインスタンスはあらゆる種類のPEMデータを保持できます。
これにより、PKCS#10認証要求など、JavaプラットフォームAPIが存在しない暗号オブジェクトを表すPEMテキストのエンコードとデコードが可能になります。
|
1 2 3 4 5 6 7 8 9 10 |
public record PEMRecord(String type, String content, byte[] leadingData) implements DEREncodable { public PEMRecord(String type, String content); public PEMRecord(String type, String content, byte[] leadingData); String type(); // Cryptographic object type, from the header text // (e.g., "PRIVATE KEY") String content(); // Base64-encoded PEM content byte[] leadingData(); // Any content preceding the PEM header } |
PEMDecoderインスタンスは、テキストのPEMタイプに対応するJavaプラットフォームAPIが存在しない場合、PEMテキストをPEMRecordオブジェクトにデコードします。
|
1 2 3 4 5 |
DEREncodable d = PEMDecoder.of().decode(pem); if (d instanceof PEMRecord pr) { throw new IllegalArgumentException("Unhandled PEM type: " + pr.type() + "; data: " + pr.content()); } |
PEMテキストの先頭データにアクセスする必要がある場合、またはテキストの内容を自身で処理したい場合は、デコード時にPEMRecordを明示的に要求できます:
|
1 |
PEMRecord pr = PEMDecoder.of().decode(pem, PEMRecord.class); |
PEMEncoderインスタンスは、その内容を検証せずにPEMRecordオブジェクトをPEMテキストにエンコードします。
EncryptedPrivateKeyInfoクラス
既存のEncryptedPrivateKeyInfoクラスは暗号化された秘密鍵を表します。
PEMEncoderおよびPEMDecoderクラスとの連携を容易にするため、以下の5つのメソッドを追加しました:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
EncryptedPrivateKeyInfo { ... public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, char[] password); public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, char[] password, String algorithm, AlgorithmParameterSpec params, Provider p); public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, Key encKey, String algorithm, AlgorithmParameterSpec params, Provider provider, SecureRandom random); public PrivateKey getKey(char[] password) throws GeneralSecurityException; public PrivateKey getKey(Key decryptKey, Provider provider) throws GeneralSecurityException; } |
3つの新しい静的encryptKeyメソッドは、指定されたパスワードで与えられたPrivateKeyを暗号化します。
高度な使用法では、デフォルトが不十分な場合に、2番目のメソッドですべての暗号化パラメータを指定できます。
返されるEncryptedPrivateKeyInfoインスタンスは、PEMエンコーダに渡してPEMテキストにエンコードできます:
|
1 2 |
var epki = EncryptedPrivateKeyInfo.encryptKey(privateKey, password); byte[] pem = PEMEncoder.of().encode(epki); |
新しい getKey メソッドは、EncryptedPrivateKeyInfo インスタンス内の秘密鍵を復号します。
これらのメソッドはパスワードと、場合によっては暗号プロバイダーを受け取り、PrivateKey を返します。
これらは、PEMDecoder が EncryptedPrivateKeyInfo を返す場合に使用できます。
|
1 2 |
EncryptedPrivateKeyInfo epki = PEMDecoder.of().decode(pem); PrivateKey key = epki.getKey(password); |
PEMEncoderまたはEncryptedPrivateKeyInfoでPrivateKeyを暗号化する際に使用されるデフォルトのパスワードベース暗号化(PBE)アルゴリズムは、デフォルトのセキュリティプロパティファイルで定義されます。
jdk.epkcs8.defaultAlgorithmセキュリティプロパティは、デフォルトアルゴリズムを「PBEWithHmacSHA256AndAES_128」と定義します。
将来的にデフォルトアルゴリズムが変更される可能性がありますが、これは現在作成されるPEMテキストには影響しません。
なぜなら、そのテキストにエンコードされたデータには、復号に必要なアルゴリズム名およびその他のすべてのパラメータが含まれているためです。
Alternatives
PEM APIはBase64と暗号オブジェクト間のブリッジです。
既存の暗号APIとの整合性が取れないため、他の多くの設計案を却下しました。
代替案の中には妥当なものもあったかもしれませんが、HexFormat APIやBase64 APIのネストされたエンコーダ/デコーダクラスとの類似性から、提案されたAPIを採用しました。
不変性、スレッドセーフ性、そしてエンコードとデコードのためのAPI内での明確なパスを確保したかったのです。
検討した代替案には以下のようなものがあります:
- EncodedKeySpec APIの拡張 — このAPIは、KeyFactoryインスタンスやその他の暗号化クラス向けにバイナリエンコードされた鍵データをカプセル化します。
新しいPEMEncodedKeySpecサブクラスは、カプセル化されたPEMテキストの型識別を行いながら、PEMテキストと適切な秘密鍵または公開鍵EncodedKeySpec間のエンコード/デコード操作を提供できます。
この設計にはいくつかの欠点がありました。
第一に、PEMEncodedKeySpecクラスが変換処理に使用されることになり、これは親クラスであるEncodedKeySpecの目的ではありません。
第二に、EncodedKeySpecは鍵中心の設計であるため、証明書や証明書失効リストをPEMテキストへエンコードする機能をサポートできません。
最後に、新しいEncodedKeySpecサブクラスは、既存のサードパーティ製暗号プロバイダーとの互換性リスクや使い勝手の問題を招く恐れがあります。 - CertificateFactory および KeyFactory API の強化 — CertificateFactory API は既に PEM 形式の証明書および証明書失効リストデータのデコードをサポートしているため、CertificateFactory および KeyFactory にエンコード方法を追加することは既存の設計と整合性があります。
CertificateFactoryはこのアプローチを容易に見せている。
なぜなら証明書には業界標準のエンコーディングが一つ存在するからだ。
一方KeyFactoryは異なるエンコーディング形式をサポートする必要がある。
さらに悪いことに、KeyFactoryインスタンスの提供者は既知の非対称鍵の全タイプをサポートする必要はない。
加えて、プロバイダーの保守担当者はPEMエンコーディングと暗号化された秘密鍵の処理の両方に責任を負わされることに難色を示す可能性がある。
このためKeyFactoryの拡張は困難な解決策となる。 - スタティックメソッド — スタティックメソッドは不変性とスレッドセーフ性に優れますが、暗号化された秘密鍵では使い勝手の問題が生じます。
暗号化された秘密鍵の変換にはパスワードが必要ですが、他の暗号オブジェクトの変換では不要です。
したがって、静的メソッドでは暗号化された秘密鍵に対して、暗号化パラメータを取る追加のオーバーロードメソッドや、EncryptedPrivateKeyInfoインスタンスの必須使用といった好ましくない解決策が必要になります。
エンコーダとデコーダに暗号化パスワードを保存させる方が、ユーザーエクスペリエンスが向上します。 - 中間PEMオブジェクトAPI — キー、証明書、証明書失効リスト、またはPEMテキストのいずれかを保持するインスタンスを持つラッパークラスを導入できる。
このクラスはエンコード/デコードメソッドを宣言するか、別個のAPIがそのインスタンスに対して操作を実行するかのいずれかとなる。
このアプローチはPEMテキストの独立した表現を提供しますが、柔軟性の過剰はマイナスです。
暗号オブジェクトとPEMテキストの両方をラップできるクラスは、これらが根本的に異なる性質のものゆえに混乱を招く可能性があります。
与えられたデータに対する明確なエンコード/デコード経路を用意することで、より誘導的なユーザー体験が実現されます。 - 単一クラスAPI — 単一のPEMクラスで符号化と復号の両方を実行することは可能ですが、静的メソッドや中間PEMオブジェクトのアプローチと同様に、符号化と復号の明確な操作パスが欠如します。
符号化と復号を別々のクラスに分離することで、各クラスが必要な操作のみを提供するため、APIはより使いやすくなります。 - 暗号プロバイダーによるエンコーディング変換サービスのサポート追加 — 暗号オブジェクトのテキスト表現とバイナリ表現間の変換サービスをサポートする暗号プロバイダーの機能追加を検討しました。
プロバイダーは既に暗号オブジェクトのインポート/エクスポートにバイナリ形式を内部で使用しており、変換サービスの追加はPEM以外の場面でも有用です。
ただしこのアプローチでは、インフラストラクチャの構築に多大な労力を要する一方で、追加されるサービスは限定的です。
同時に、既存プロバイダーの互換性が損なわれるリスクがあり、APIの使用も複雑化します。 - 汎用暗号化符号化・復号化APIの導入 — 多くのテキスト形式で使用可能な汎用APIを検討しました。
しかし、これらの形式は鍵、証明書チェーン、圧縮、その他のオプションのサポートにおいて特徴が異なるため、この案は却下されました。
1つのAPIに複数の形式を統合し、一部のメソッドを形式固有にすると混乱を招くためです。
Testing
テストには以下が含まれます:
- サポートされているすべてのDEREncodableクラスがPEMテキストをエンコードおよびデコードできることを確認します。
- RSA、EC、およびEdDSA暗号オブジェクトがエンコードおよびデコード可能であることを検証する。
- サードパーティ製アプリケーションが生成したPEMテキストの読み取り、およびその逆。
- PEMテキストが不適切なネガティブテスト
以上、OpenJDK JEP 470: PEM Encodings of Cryptographic Objects (Preview)より
JEP 470 Code Example
JEP 470を試すために自己署名による暗号化秘密鍵と証明書チェーンを一つのPEMファイルとして作成します。
そのPEMファイルを、その読込、復号、公開鍵一致、PKIX検証をJEP 470を使って簡単なプログラムを組んでみます。
作業環境は、
Windows11 24H2
PowerShell 7.5.3
Openssl 3.5.3 Light
JEP 470はPreview版なので仕様変更によって今回のプログラムでは正しく動かなくなるかもしれません。
PEMファイル作成
PEM形式は、鍵や証明書のデータをBase64でエンコードし、ヘッダー/フッターで囲んで保存するASCIIテキストです。
一つのファイルに複数オブジェクトを連続で格納することも可能です(たとえば秘密鍵+サーバ証明書+中間CAルート証明書)。
それでは、自己署名による簡易的なPEMファイルを作成します。
PowerShellを立ち上げて次のようにコマンドを実行します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# 1. 自己署名証明書を作成(RSA 2048bit)7日間有効 $cert = New-SelfSignedCertificate ` -Subject "CN=TestCert" ` -KeyAlgorithm RSA ` -KeyLength 2048 ` -KeyExportPolicy Exportable ` -CertStoreLocation "Cert:\CurrentUser\My" ` -NotAfter (Get-Date).AddDays(7) # 2. 秘密鍵を PKCS#12 (PFX) として一時エクスポート $pfxPath = "$env:TEMP\testcert.pfx" $pwd = ConvertTo-SecureString -String "password" -Force -AsPlainText Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $pwd # 3. OpenSSL を使って PFX → 暗号化 PKCS#8 PEM に変換 # (Windows に OpenSSL がインストールされている前提) $privatePem = "$env:TEMP\encrypted_key.pem" $certPem = "$env:TEMP\cert.pem" $finalPem = "$env:USERPROFILE\Desktop\PEM.pem" # 秘密鍵を PKCS#8 形式で暗号化して出力 # 秘密鍵を Bag Attributes なしで出力 openssl pkcs12 -in $pfxPath -nocerts -nodes -passin pass:password | openssl pkcs8 -topk8 -v2 aes-256-cbc -passout pass:password -out $privatePem # 証明書を PEM 形式で出力 # 証明書を Bag Attributes なしで出力 openssl pkcs12 -in $pfxPath -clcerts -nokeys -passin pass:password | openssl x509 -out $certPem # 4. 秘密鍵と証明書を 1つの PEM に結合 Get-Content $privatePem, $certPem | Set-Content $finalPem -Encoding ascii Write-Host "PEM bundle created at: $finalPem" |
デスクトップにPEM.pemが作成されます。
JEP 470 PEMファイル読み込み、複合、公開鍵一致、PKIX検証プログラム
JEP 470: PEM Encodings of Cryptographic Objectsは、従来難しかったPEM形式の暗号鍵、証明書のエンコード/デコード操作に特化した新API群(PEMDecoder,PEMEncoder ,PEMRecord ,DEREncodable 等)です。
この機能によって、暗号化されたPKCS#8秘密鍵(“PRIVATE KEY”や“ENCRYPTED PRIVATE KEY”)、証明書チェーン(“CERTIFICATE”)が一つになったPEMファイルをJavaのみで直接解析・復号・公開鍵抽出・一致確認・証明書パス(PKIX)検証まで処理することが可能となります。
それでは、シンプルなプログラムを組んでみます。
プログラムの動作はコメントとして書き込んであるので参考になれば幸いです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
package jep470preview; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.DEREncodable; import java.security.InvalidAlgorithmParameterException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PEMDecoder; import java.security.PEMEncoder; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.CertPath; import java.security.cert.CertPathValidator; import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.PKIXCertPathValidatorResult; import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPublicKeySpec; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * * @author 0xCAFEBABE */ public class JEP470PREVIEW { public static void main(String[] args) { keyMatchVerification(); } private static void keyMatchVerification() { // PEMファイルのパスワード char[] keyPassword = "password".toCharArray(); // 暗号化PEMファイルを読み込んで検証 try (InputStream in = JEP470PREVIEW.class.getResourceAsStream("resources/PEM.pem")) { // PEMDecoderで内容をデコード(指定されたパスワードを使用してすべての暗号化された秘密鍵 PEM データを復号するようにデコーダを設定) PEMDecoder decoder = PEMDecoder.of().withDecryption(keyPassword); PrivateKey privateKey = null; List<X509Certificate> certChain = new ArrayList<>(); while (in.available() > 0) { // InputStreamからDEREncodableをデコードして返します。 DEREncodable derEnc = decoder.decode(in); // 暗号オブジェクトの型を識別(パターンマッチング使用) switch (derEnc) { case PrivateKey key -> privateKey = key; case X509Certificate cert -> certChain.add(cert); default -> { IO.println("Oh my God!\n" + derEnc.toString() + "\nThis is totally unexpected!\n"); } } } // 秘密鍵と証明書が揃ったか確認 if (privateKey == null || certChain.isEmpty()) { throw new IllegalStateException("秘密鍵または証明書が不足しています"); } // 証明書チェーンの先頭から公開鍵を抽出 PublicKey publicKeyFromCert = certChain.get(0).getPublicKey(); // 秘密鍵から公開鍵を導出(RSA) RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey; RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent() ); PublicKey derivedPublicKey = KeyFactory.getInstance("RSA").generatePublic(publicKeySpec); // 鍵の一致判定(RSA の場合は modulus と exponent の両方が一致で true) if (!publicKeyFromCert.equals(derivedPublicKey)) { throw new SecurityException("証明書チェーンと秘密鍵が一致しません!"); } IO.println("秘密鍵と証明書先頭の公開鍵が一致しました。"); // PKIX証明書パス検証(ルート自己署名の場合は自分自身がTrustAnchor) // X.509形式の証明書を扱うためのファクトリ(生成器)を取得 CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); // 証明書の連鎖(チェーン)を表すオブジェクト取得 CertPath certPath = certFactory.generateCertPath(certChain); // PKIX検証の基準点 チェーンの最後の証明書(ルートCA)、名前制約を指定しない TrustAnchor anchor = new TrustAnchor(certChain.get(certChain.size() - 1), null); // PKIX検証のためのパラメータオブジェクト(信頼できるルート証明書(TrustAnchor)の集合)で初期化 PKIXParameters params = new PKIXParameters(Set.of(anchor)); // 証明書チェーン検証時の失効確認(失効確認をスキップ) params.setRevocationEnabled(false); // 証明書パス検証を行うためのバリデータを生成(検証アルゴリズムとしてPKIXを指定) CertPathValidator validator = CertPathValidator.getInstance("PKIX"); // PKIX 検証の結果を取得 PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(certPath, params); IO.println("\n***Detailed Results***\n" + result.toString() + "\n***Results Complete***\n"); IO.println("証明書チェーンのPKIX検証に成功しました!"); // おまけ DEREncodableオブジェクトをPEMにエンコード PEMEncoder pe = PEMEncoder.of(); // 秘密鍵をバイト配列でPEMエンコード byte[] pem = pe.encode(privateKey); String text = new String(pem, StandardCharsets.UTF_8); IO.println("\n秘密鍵をバイト配列でPEMエンコード\n" + text + "\n"); // 公開鍵をPEMエンコード String pubPem = pe.encodeToString(publicKeyFromCert); IO.println("\n公開鍵をPEMエンコード\n" + pubPem + "\n"); // 公開鍵/秘密鍵ペアを文字列としてPEMエンコード(公開鍵はエンコードされない) String pemStr = pe.encodeToString(new KeyPair(publicKeyFromCert, privateKey)); IO.println("\n公開鍵/秘密鍵ペアを文字列としてPEMエンコード\n" + pemStr + "\n"); // withEncryptionメソッドを使用して暗号化PEMエンコード(パスワードはバイト配列) String strPem = pe.withEncryption(keyPassword).encodeToString(privateKey); IO.println("\nwithEncryptionメソッドを使用して暗号化PEMエンコード\n" + strPem + "\n"); } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | CertificateException | InvalidAlgorithmParameterException | CertPathValidatorException ex) { System.getLogger(JEP470PREVIEW.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex); } } } |
「おまけ」として、DEREncodableオブジェクトをPEMにエンコードも試しています。
JEP 470はプレビュー機能であるため、このプログラムをコンパイル、実行するには下記オプションが必須です。
コンパイル時 追加コンパイラオプション
javac –enable-preview もしくは、–enable-preview -Xlint:preview
実行時 VMオプション
java –enable-preview
JEP470PREVIEW Execution Results
このプログラムの実行結果は次のようになります。








このプログラムが期待通りに動いたのでJEP 470で次のことが確認できました。
JEP 470のPEMエンコード/デコードAPIは、イミュータブル・型安全・スレッドセーフな構造で、長年の「JavaのPEMサポート不足」の根本解決をもたらす画期的な仕様です。
従来は外部ライブラリ依存が必須であった以下の処理が、
1.暗号化PKCS#8秘密鍵の復号
2.証明書チェーンのPEM一括読込
3.秘密鍵⇔証明書公開鍵の一致検証
4.PKIXパス検証(RFC5280/Java標準PKI連携)
いずれもJava25 APIのみで、安全かつ簡素に実現可能となります。
このプログラムではPEMRecordを使ってませんが、withEncryptionメソッドを使用して暗号化PEMエンコードしたString strPemを使って下記のコードを追加すると動かすことができました。
|
1 2 3 4 5 |
PEMRecord pr = PEMDecoder.of().decode(strPem, PEMRecord.class); IO.println(pr.type()); // ENCRYPTED PRIVATE KEY IO.println(pr.content()); // MIIFP...XKH9f IO.println(pr.leadingData()); // null IO.println(pr.toString()); // -----BEGIN ENCRYPTED PRIVATE KEY----- ... -----END ENCRYPTED PRIVATE KEY----- |
PEMRecordクラスは、PEM形式の鍵や証明書を読み取り、PEMRecord として扱えるようになっています。
これにより、サポート外の暗号オブジェクトを表すPEMテキストのエンコードとデコードを可能とします。
JEP 470: PEM Encodings of Cryptographic Objects — まとめ —
これまではBouncyCastle等の外部ライブラリを使用していたのをJava標準APIだけで完結できる未来が見えてきました。
これによって、外部ライブラリに依存しないため脆弱性や将来の互換対応が容易になると思われます。
JEP 470のPEMエンコード/デコードAPIは、イミュータブル・型安全・スレッドセーフな構造で、長年の「JavaのPEMサポート不足」の根本解決をもたらす画期的な仕様です。
なんで今頃?
2022年4月のJava暗号化拡張機能調査でいったい何があったんだ?
といった疑問もあると思われますが素直にこの機能拡張を喜びましょう!
Java is still great even after 30 years!


コメント