Use of HPKE with COSE 検討
IETF COSE WGで検討されている"Use of Hybrid Public-Key Encryption (HPKE) with CBOR Object Signing and Encryption (COSE)" の現状の方針がどうにも違和感があるので、つらつら対案を考えてみる。
背景・問題
現状の方針の中で特に問題だと思っているのが、enc (encapsulated key, 規格化されているKEMアルゴリズムの範囲では、送信者の生成したエフェメラル公開鍵)のエンコード形式としてCOSEの既存のephemeral keyの仕様を踏襲しようとしているところなのだが、これは前回のIETF114で方針として決まってしまったらしい。
具体的には、HPKE仕様ではKEMの出力であるencをオクテット文字列として規定しているのだが、これを COSE_Key型に無理やりマッピングしようとしているところが気に入らない(ちなみにCOSE_KeyはJWKのバイナリ版)。具体的には以下のような問題があると考えている。
- encがCOSE_Keyに素直にマッピングできる保証が将来的に確約されているわけでは無いので、新たなHPKEの暗号スイートが規定されても即座にCOSE上で使えない。HPKEに新たな暗号スイートが追加されるたびに、COSE_Keyへのマッピング仕様の策定(含むIANAへの登録)、COSEライブラリ内でenc->COSE_Keyの変換実装を追加する対応が必要になる。ライブラリ実装者としては看過できない。
- RecipientがHPKEの暗号スイートの候補を複数を提示した場合において、Senderが何を選択したかをRecipientに伝える必要があるが、AEADとKEMは良いとして、KDFを伝える手段が欠落しているように見える。
- 必須パラメータであるkty (と crv) を指定する意味がない。バイトのムダ。
- crvはKEMのアルゴリズム情報を入れるために使われているが、これは本来のcrvの定義に即していない。乱用にあたるのではないか?
- 規格上、COSE_Keyのパラメータの扱いを明確に規定する必要があるし、これに即する実装上の対応も必要となる。(例えば、key_opsに何を入れるのか、deriveKeyなのかderiveBitsも許すのか?等)
対案
上に挙げた問題を解消する対案を考えてみる。
- COSE Header Parameterの
alg
値として、"HPKE" を新たに定義する。現ドラフトのようにAEADアルゴリズム情報は入れない。 - ephemeral keyを流用せず、HPKE algのパラメータとして新たに HPKE sender information 構造を導入する。
96(
[
/ algorithm id TBD1 for COSE_ALG_HPKE /
<< {1: TBD1} >>,
{
/ HPKE sender information structure /
-4 (TBD2): << {
/ KEM identifier, 2bytes (optional) /
-1: 0x0010,
/ HPKE symmetric cipher suite (KDF id + AEAD id), 4bytes (required) /
1: 0x00010001,
/ HPKE encapsulated key (required) /
2: h'xxxxxxxxxxxxxxxxxxxxxxxxxxx',
} >>,
/ the recipient's public key identifier (required) /
4: 'kid-2'
},
/ encrypted plaintext /
h'4123E7C3CD992723F0FA1CD3A903A58842B1161E02D8E7FD842C4DA3B984B9CF'
]
)
上に挙げた現ドラフトの問題点は解消されている。
- 一度、上記の新たなCOSEパラメータを定義してしまえば、HPKEの暗号化スイートが追加されても、COSE-HPKEとして新たなIANAレジストリの登録やCOSE_Keyへの変換方法などを規定する必要は無いし、ライブラリの実装変更も必要ない
- KDFの情報も伝えられている
- 必要最小限の情報に絞られており省スペース
- HPKE sender情報表現に特化した構造なので、メッセージサイズ最小化を追求することも可能。例えば、上記例はオブジェクト型を採用しているが kdf_id + aead_id + enc のbstr型とすることもできる。
- 既存パラメータの本来の意味を逸脱した誤用が無い
- COSE_Key採用に伴う、無駄なパラメータ検証の実装が必要無い
従来方式との比較
現ドラフトの従来方式と対案を比較する。
1. HPKEへの新規暗号スイートの追加がCOSE側の仕様や実装に影響を与えないか?
- current draft: poor
- 影響を与える。新たなHPKE暗号スイートが定義される度に、仕様変更(COSE IANAレジストリへの登録など)や既存ライブラリへの追加実装が必要である。
- Ilari's proposal: good
- 影響を与えない。
- my proposal: good
- 影響を与えない。
2. HPKEの暗号スイートを動的にネゴシエーションできる仕組みになっているか?
- current draft: poor
- KDFの情報が欠落しており柔軟なネゴシエーションができない。暗黙的にKEMとKDFの組み合わせに制約がある仕様となっている。汎用層の仕様としては問題があると言わざるを得ない。
- Ilari's proposal: good--
- はい。ただし、COSE_Keyの構造はそのままrecipient public keyを表現する構造にも利用されることになるが、許容されるkdfとaeadの組み合わせ毎に鍵情報を表現しなければならず効率がわるい。
- my proposal: good
- はい。Recipientが複数候補を提示し、Senderが1つの暗号スイートを選択し、RecipientにEncと共に通知する仕組みになっている。
3. 必要な情報に絞られているか?メッセージサイズが最適化できるか?
- current draft: poor
- いいえ。ktyやcrvが必要。例ではcrvに既存の楕円曲線識別子が入っているが、Appendixにはcrvに
- Ilari's proposal: moderate
- いいえ。ktyが必要。ヘッダにも alg=HPKEが指定されているので冗長であり不要。
- my proposal: good
- はい。最適化するならば、kdf_id + aead_id + enc を纏めて1つのbstrデータとする方法もある。
4. 実装が容易か?
- current draft: poor
- いいえ。新たなKEMが定義されるたびに、(kty、crv、alg、kty毎のキーフォーマット) => (kem, kdf, aead, 生公開鍵) の変換実装をCOSEライブラリ内に実装する必要がある。新たなAEADや(おそらくKDF)が定義されるたびにCOSE_Key.algへの実装上のマッピングテーブルを変更する必要もある。新規暗号スイートの追加によって非常に複雑な実装をCOSEライブラリ内部に行わなければならない可能性もある。
- Ilari's proposal: good--
- はい。ただし、不要なCOSE_Keyパラメータ(key_ops, iv, kty)のバリデーションが必要。
- my proposal: good
- はい。Recipientが複数候補を提示し、Senderが1つの暗号スイートを選択し、RecipientにEncと共に通知する仕組みになっている。
5. 既存仕様を拡大解釈した乱用が無いか?
- current draft: poor
- COSE_Key.algについて、含まれる鍵本体とは直接関係のないAEADアルゴリズムが含まれている点は奇妙である。Appendixに記載されているCOSE_Key.crvにハッシュアルゴリズムの情報が含まれている。
- Ilari's proposal: good
- 無い。kty=HPKEは微妙なところだが他に手は無い。
- my proposal: good
- 無い。
スライドにまとめてみた(英語)。
Recipient Public Key について考える
Use of HPKE with COSE がencにフォーカスしているので当然なのだが、ここまでは、enc (規格化されているKEMアルゴリズムの範囲では、エフェメラルな Sender Public Key) に絞った話だった。これはCOSEライブラリの内部で生成・消費されるものであり、外部インタフェースに出てこないのでCOSE_Key型で表現できる必要は無い、という側面もあった。
一方で、recipient public keyの場合は事情が異なる。
recipient public keyは当然ながら COSEライブラリのインタフェースで指定する必要があるため、COSE_Key構造で表現できる必要がある。また、HPKEのアプリケーション層での利用を考えた場合、JWKSとしてHPKEの recipient public key が公開され、これを使ってHPKEベースのE2E暗号を行う、というのは十分に想定されるユースケースである。recipient public key は JWKや COSE_Keyとして表現できるべきである。
少なくとも、HPKEの拡張仕様を策定するたびにJWKおよびCOSE_Keyに関連したIANAレジストリへの登録を必要ないようにしつつ、従来のJWKの利用と互換性のある範囲で仕様を定めるなら、私案としては以下のようになる(jwks_uriのレスポンス例)。COSE_Keyは、これを素直にバイナリ形式に落とせばよい。
{
keys: [
// all of the attributes below are required other than 'priv',
{
// key type: MUST be "HPKE".
"kty": "HPKE",
// recipient public key identifier.
"kid": "x25519-01",
// KEM id: DHKEM(X25519, HKDF-SHA256)
"kem": 0x0020,
// acceptable KDF ids: All of the currently defined KDFs are acceptable.
"kdfs": [0x0001, 0x0002, 0x0003],
// acceptable AEAD ids: All of the currently defined AEADs are acceptable.
"aeads": [0x0001, 0x0002, 0x0003],
// base64-encoded recipient public key.
"pub": "2E6dX83gqD_D0eAmqnaHe1TC1xuld6iAKXfw2OVATr0",
// base64-encoded recipient private key. OPTIONAL.
"priv": "vsJ1oX5NNi0IGdwGldiac75r-Utmq3Jq4LGv48Q_Qc4",
}
]
}
この案の懸念点が2つある。
- kty=="HPKE"を持つJWK(or COSE_Key)化したrecipient public keyだけでは、HPKEの暗号スイートが確定しないこと。
- Senderが暗号スイート(特にKDFとAEAD)を明示的に指定し、HPKEライブラリ内部では 指定した暗号スイートが recipient public keyの"kdfs"および"aeads"にそれぞれ含まれることを確認する必要がある。
- 既存のJWK(kty=="OKP" or "EC")をどう扱うかの規定が悩ましいこと。
- 既存の(KEMにつかえる)ktyに、kem, kdfs, aeadsを定義して、この指定が無いとHPKEでは使えないとするか?RecipientのサポートするHPKEの暗号スイートが別手段で共有されている条件のもと、kem, kdfs, aeads無しでも使えるとするか?そもそも既存のktyでHPKEを利用することをゆるすか?
- 私見では、シンプルに既存のktyのJWK (or COSE_Key)でのHPKE利用は禁止、で良いと思うが、Web Cryptography APIの実装を待つ必要があるなど実現性に難がある。
この話は、HPKEのアプリケーション層での応用促進の観点で重要だと思うので、IETFががっつり議論してくれるとよいのだが。
上記、アイデアをブラッシュアップしてIETFにinternet-draftとして提出した。
Appendix A: 疑似コード
整理のために疑似コードを書いてみる。
# for sender:
def cose_encrypt(public_key: COSEKey, plain_text: bytes, ?alg: number) -> bytes:
# s-0: recognizes COSE-HPKE with alg (and public_key).
# >>> COSE-HPKE process
# s-1: determines a specific HPKE cipher suite which consists of KEM, KDF and AEAD.
hpke_cipher_suite = determine_hpke_cipher_suite(public_key, ?alg, ?sender_preference);
# s-2: coverts COSEKey-typed public key to raw key.
raw_public_key = public_key.to_bytes();
# s-3: calls HPKE seal function which returns a cipher text and an encapsulated key.
cipher_text, enc = hpke_seal(hpke_cipher_suite, raw_public_key, plain_text, ?aad);
# s-4: build HPKE sender information that should be informed to the recipient.
# ** This step is only the scope of this spec. **
sender_info = build_sender_info(hpke_cipher_suite, enc);
# s-5: builds a cose message and returns it.
encrypted_cose_msg = build_cose_msg(public_key.kid, alg, sender_info, cipher_text);
return encrypted_cose_msg;
# for recipient:
def cose_decrypt(private_keys: Array[COSEKey], encrypted_cose_msg: bytes) -> bytes:
# r-0: gets alg from the encrypted_cose_msg and recognizes COSE-HPKE with alg.
alg = get_alg(encrypted_cose_msg);
# >>> COSE-HPKE process
# r-1: parses the encrypted_cose_msg
# and extracts information required for HPKE decryption.
kid, sender_info, cipher_text = parse_cose_msg(encrypted_cose_msg);
# r-2: parses sender_info.
hpke_cipher_suite, enc = parse_sender_info(sender_info);
# r-3: determines a specific private key
# and validates that the key can be used for HPKE decryption.
private_key = private_keys.get(kid);
# r-4: coverts COSEKey-typed private key to raw key.
raw_private_key = private_key.to_bytes();
# r-5: calls HPKE open function which returns decrypted plain text.
plain_text = hpke_open(hpke_cipher_suite, raw_private_key, enc, cipher_text, ?aad);
return plain_text;
提案方式がドラフトに採用された。