JEP 470: PEM Encodings of Cryptographic Objects (Preview)

JDK
目次

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です:

許可されているクラスおよびインターフェースの一部について、対応する調整を行います:

エンコーディング
PEMEncoderクラスは、DEREncodableオブジェクトをPEMテキストにエンコードするためのメソッドを宣言します:

DEREncodableオブジェクトをエンコードするには、まずof()を呼び出してPEMEncoderインスタンスを取得します。
返されるインスタンスはスレッドセーフで再利用可能なため、そのエンコードメソッドを繰り返し使用できます。
エンコードには2つの方法があります。1つは、ISO-8859-1文字セットでエンコードされた文字を含むバイト配列でPEMテキストを返す方法です。
例えば、秘密鍵をエンコードするには:

もう一方のエンコード方法はPEMテキストを文字列として返します。
例えば、公開鍵/秘密鍵ペアを文字列にエンコードするには:

秘密鍵をエンコードする場合は、withEncryptionメソッドを使用して暗号化できます。
このメソッドはパスワードを受け取り、そのパスワードで鍵を暗号化するよう設定された新しい不変のPEMEncoderインスタンスを返します:

このように構成されたPEMEncoderは、PrivateKeyオブジェクトのみをエンコードできます。
デフォルトの暗号化アルゴリズムを使用します。
非デフォルトの暗号化パラメータを使用する場合、または別の暗号化プロバイダーで暗号化するには、EncryptedPrivateKeyInfoオブジェクトを使用してください(以下を参照)。
デコーディング
PEMDecoderクラスは、PEMテキストをDEREncodableオブジェクトにデコードするためのメソッドを宣言します:

PEMテキストをデコードするには、まずof()を呼び出してPEMDecoderインスタンスを取得します。
返されるインスタンスはスレッドセーフで再利用可能なため、そのデコードメソッドを繰り返し使用できます。
デコードには4つのメソッドがあり、それぞれDEREncodableオブジェクトを返します。
instanceof演算子やswitch文を用いたパターンマッチングで、返される暗号オブジェクトの型を識別できます。
例えば、公開鍵または秘密鍵のいずれかをエンコードすると予想されるPEMテキストをデコードするには:

暗号化されたオブジェクトの型を事前に知っている場合、Class引数を取るデコードメソッドのいずれかに、対応するクラスを渡すことができます。
これにより、メソッドの結果の型に対してパターンマッチングを行う必要や、型をチェックしてキャストする手間を省けます。
例えば、型がECPublicKeyであると分かっている場合:

この場合、クラスが正しくない場合、ClassCastExceptionがスローされます。
入力PEMテキストが秘密鍵をエンコードしている場合、withDecryptionメソッドを使用して復号できます。
このメソッドはパスワードを受け取り、鍵をPrivateKeyオブジェクトに復号するように構成された新しいPEMDecoderインスタンスを返します。
このように構成されたPEMDecoderは、暗号化されていないオブジェクトのデコードも引き続き可能です。
例えば、ECPrivateKeyを復号するには:

秘密鍵をエンコードしたPEMテキストを復号化する際、パスワードを指定しない場合、復号メソッドはEncryptedPrivateKeyInfoインスタンスを返します。
このインスタンスを使用して復号化し、PrivateKeyオブジェクトを生成できます(下記参照)。
状況によっては、PEMテキストをデコードする際に特定の暗号プロバイダーを使用する必要が生じる場合があります。
withFactoryメソッドは、指定されたプロバイダーを使用して暗号オブジェクトを生成する新しいPEMDecoderインスタンスを返します。
例えば、特定のプロバイダーで証明書をデコードするには:

プロバイダが要求された種類の暗号オブジェクトを生成できない場合、IllegalArgumentException がスローされます。
PEMテキストを暗号オブジェクトにデコードする際、入力文字列またはバイトストリーム内のPEMヘッダーより前のデータはすべて無視されます。
そのデータが必要な場合は、PEMRecordオブジェクトにデコードすることで取得できます。
PEM入力が解析できない場合、IllegalArgumentExceptionがスローされます。
入力ストリームから読み取られるバイトは、ISO-8859-1文字セットでエンコードされた文字を表すとみなされます。
PEMRecordクラス
PEMRecordクラスはDEREncodableを実装します。
そのインスタンスはあらゆる種類のPEMデータを保持できます。
これにより、PKCS#10認証要求など、JavaプラットフォームAPIが存在しない暗号オブジェクトを表すPEMテキストのエンコードとデコードが可能になります。

PEMDecoderインスタンスは、テキストのPEMタイプに対応するJavaプラットフォームAPIが存在しない場合、PEMテキストをPEMRecordオブジェクトにデコードします。

PEMテキストの先頭データにアクセスする必要がある場合、またはテキストの内容を自身で処理したい場合は、デコード時にPEMRecordを明示的に要求できます:

PEMEncoderインスタンスは、その内容を検証せずにPEMRecordオブジェクトをPEMテキストにエンコードします。
EncryptedPrivateKeyInfoクラス
既存のEncryptedPrivateKeyInfoクラスは暗号化された秘密鍵を表します。
PEMEncoderおよびPEMDecoderクラスとの連携を容易にするため、以下の5つのメソッドを追加しました:

3つの新しい静的encryptKeyメソッドは、指定されたパスワードで与えられたPrivateKeyを暗号化します。
高度な使用法では、デフォルトが不十分な場合に、2番目のメソッドですべての暗号化パラメータを指定できます。
返されるEncryptedPrivateKeyInfoインスタンスは、PEMエンコーダに渡してPEMテキストにエンコードできます:

新しい getKey メソッドは、EncryptedPrivateKeyInfo インスタンス内の秘密鍵を復号します。
これらのメソッドはパスワードと、場合によっては暗号プロバイダーを受け取り、PrivateKey を返します。
これらは、PEMDecoder が EncryptedPrivateKeyInfo を返す場合に使用できます。

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を立ち上げて次のようにコマンドを実行します。

デスクトップに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)検証まで処理することが可能となります。
それでは、シンプルなプログラムを組んでみます。
プログラムの動作はコメントとして書き込んであるので参考になれば幸いです。

「おまけ」として、DEREncodableオブジェクトをPEMにエンコードも試しています。
JEP 470はプレビュー機能であるため、このプログラムをコンパイル、実行するには下記オプションが必須です。
コンパイル時 追加コンパイラオプション
javac –enable-preview もしくは、–enable-preview -Xlint:preview
実行時 VMオプション
java –enable-preview

JEP470PREVIEW Execution Results

このプログラムの実行結果は次のようになります。

Output1
Output2
Output3
Output4
Output5
Output6
Output7
Output8

このプログラムが期待通りに動いたので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を使って下記のコードを追加すると動かすことができました。

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!

コメント

コメントする

目次