Open8

HPKE利用規格調査メモ

dajiajidajiaji

HPKEを利用する上位の規格がどのようにHPKEを使っているのか(動作モード、公開鍵情報の構造や共有方法、INFOやAADをどう使っているか等)を調査する。

目的:

  • 自製のHPKEモジュール(hpke-js) へのフィードバック
  • HPKEの使い方のベストプラクティス把握
  • アプリケーション層での応用アイデア創出・ユースケース抽出
dajiajidajiaji

Encrypted Client Hello (ECH)

TLS の ClientHello も暗号化するやつ。

1. 動作モード

Baseのみ

2. 公開鍵

データ構造

Recipientの公開鍵のデータ構造は下記HpkeKeyConfig。1つの公開鍵にkem_idと受け入れ可能な kdf_idとaead_idのペアのリスト(cipher_suites)が紐づけられている。

opaque HpkePublicKey<1..2^16-1>;
uint16 HpkeKemId;  // Defined in I-D.irtf-cfrg-hpke
uint16 HpkeKdfId;  // Defined in I-D.irtf-cfrg-hpke
uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke

struct {
    HpkeKdfId kdf_id;
    HpkeAeadId aead_id;
} HpkeSymmetricCipherSuite;

struct {
    uint8 config_id;
    HpkeKemId kem_id;
    HpkePublicKey public_key;
    HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>;
} HpkeKeyConfig;

管理方法

鍵ローテーションに関する言及なし。

共有方法 (Recipient -> Sender)

ECHConfigのリスト(下記ECHConfigList)を公開する。共有方法自体は Out-of-Scope。一例としては、DNSのHTTPS RR(Service binding and parameter specification via the DNS (DNS SVCB and HTTPS RRs))が挙げられている。

struct {
    HpkeKeyConfig key_config;
    uint8 maximum_name_length;
    opaque public_name<1..255>;
    Extension extensions<0..2^16-1>;
} ECHConfigContents;

struct {
    uint16 version;
    uint16 length;
    select (ECHConfig.version) {
        case 0xfe0d: ECHConfigContents contents;
    }
} ECHConfig;

ECHConfig ECHConfigList<1..2^16-1>;

共有方法 (Sender -> Recipient)

enc(encapsulated key)は、ECH extensionのペイロードして送る。encに加えて、選択したkdf_id/aead_idのペアとconfig_id。config_idは1バイトなのでkem_id(2バイト)を送るより1バイト少なくできている。

enum { outer(0), inner(1) } ECHClientHelloType;

struct {
   ECHClientHelloType type;
   select (ECHClientHello.type) {
       case outer:
           HpkeSymmetricCipherSuite cipher_suite;
           uint8 config_id;
           opaque enc<0..2^16-1>;
           opaque payload<1..2^16-1>;
       case inner:
           Empty;
   };
} ECHClientHello;

3. 暗号・復号

INFO (Application-supplied INFOrmation)

固定文字列"tls ech"とシリアライズした上記 ECHConfig。間に1byte(0x00)のセパレータを入れる。

"tls ech" || 0x00 || ECHConfig

AAD (Additional Authenticated Data)

ClientHelloInnerの攻撃者による置き換えを防ぐために、AAD(ClientHelloOuterAAD)を入れ、ClientHelloOuterを認証する。AADはClientHelloOuterのペイロードを0埋めしてシリアライズしたもの。

final_payload = context.Seal(ClientHelloOuterAAD,
                             EncodedClientHelloInner)
dajiajidajiaji

Oblivious DNS Over HTTPS (ODoH)

DoH (DNS over HTTPS) のクエリ・レスポンスを暗号化する。

1. 動作モード

  • Baseのみ
  • レスポンスの暗号化に export-only AEAD を用いる

2. 公開鍵

データ構造

ECHと違い、HPKE暗号スイート(kem_id & kdf_id & aead_id)に公開鍵を対応付けている。ECHの設計の方が汎用性が高い気がするが、割り切りとしてはアリか。

struct {
   uint16 kem_id;
   uint16 kdf_id;
   uint16 aead_id;
   opaque public_key<1..2^16-1>;
} ObliviousDoHConfigContents;

管理方法

鍵ローテーションに関する言及あり。鍵は毎日更新することを推奨(RECOMMENTED)している。

共有方法 (Recipient -> Sender)

ODoHプロトコルのバージョン情報が入った公開鍵情報リスト(下記ObliviousDoHConfigs)を共有する。ObliviousDoHConfigsは推奨順にならべる。共有方法自体は Out-of-Scope。

struct {
   uint16 version;
   uint16 length;
   select (ObliviousDoHConfig.version) {
      case 0x0001: ObliviousDoHConfigContents contents;
   }
} ObliviousDoHConfig;

ObliviousDoHConfig ObliviousDoHConfigs<1..2^16-1>;

共有方法 (Sender -> Recipient)

Senderのencは、暗号化されたクエリの先頭に付与される。

Q_encrypted = enc || ct

暗号化されたクエリ(Q_encrypted)を運ぶ ODoHのメッセージは以下の構造をとる。

struct {
   uint8  message_type;
   opaque key_id<0..2^16-1>;
   opaque encrypted_message<1..2^16-1>;
} ObliviousDoHMessage;

message_typeはクエリ(0x01)かレスポンス(0x02)か、クエリ時のkey_idは上記ObliviousDoHConfigContentsを使って以下のように生成される。したがって選択されたHPKE暗号スイートと公開鍵の情報はkey_idを介してRecipient(サーバ)側に伝わる。

Expand(Extract("", config), "odoh key id", Nh)

なお、レスポンス時のkey_idには、Recipient側で以下のように生成したresp_nonceを指定する。

resp_nonce = random(max(Nn, Nk)

3. 暗号・復号

INFO (Application-supplied INFOrmation)

固定文字列のみ。クエリには "odoh query"、レスポンスには "odoh response"。

AAD (Additional Authenticated Data)

クエリの暗号・復号に用いるAADは以下の通り。message_typekey_idの長さ、key_id自体をコンカテしたものを用いる。

0x01 || len(key_id) || key_id

レスポンスの暗号・復号に用いるAADは以下の通り。message_typeとレスポンスとして受信したresp_nonceの長さ、resp_nonce自体をコンカテしたものを用いる。

0x02 || len(resp_nonce) || resp_nonce

bi-directionalの実現方法

レスポンスの暗号・復号方法は以下の通り。

Recipient側でresp_nonceを以下のように生成し、これをレスポンスのODoHメッセージのkey_idに指定する。

resp_nonce = random(max(Nn, Nk)

Sender側、Recipient側双方、以下の関数でkeyとnonceを生成する。

def derive_secrets(context, Q_plain, resp_nonce):
  secret = context.Export("odoh response", Nk)
  salt = Q_plain || len(resp_nonce) || resp_nonce
  prk = Extract(salt, secret)
  key = Expand(odoh_prk, "odoh key", Nk)
  nonce = Expand(odoh_prk, "odoh nonce", Nn)
  return key, nonce

Recipient側は以下のように暗号する。

def encrypt_response_body(R_plain, aead_key, aead_nonce, resp_nonce):
  aad = 0x02 || len(resp_nonce) || resp_nonce
  R_encrypted = Seal(aead_key, aead_nonce, aad, R_plain)
  return R_encrypted

Sender側は以下のように復号する。

def decrypt_response_body(context, Q_plain, R_encrypted, resp_nonce):
  aead_key, aead_nonce = derive_secrets(context, Q_plain, resp_nonce)
  aad = 0x02 || len(resp_nonce) || resp_nonce
  R_plain, error = Open(key, nonce, aad, R_encrypted)
  return R_plain, error
dajiajidajiaji

Firmware Encryption with SUIT Manifests

ファームウェアの暗号化にHPKEをつかう。

IETF SUIT(Software Updates for Internet of Things) WG で仕様策定が進められているIoTデバイスのファームウェア更新規格の1つ。正確にはファームウェア暗号化キーの暗号化にHPKEをつかう。

1. 動作モード

Baseモードのみ

2. 公開鍵

データ構造

COSE (CBOR Object Signing and Encryption)に公開鍵情報を入れる。具体的な仕様は Use of HPKE with COSE で策定中。

管理方法

言及なし。

共有方法 (Recipient -> Sender)

言及なし。

共有方法 (Sender -> Recipient)

Use of HPKE with COSE で規定中。

3. 暗号・復号

INFO (Application-supplied INFOrmation)

言及なし。

AAD (Additional Authenticated Data)

言及なし。

dajiajidajiaji

Oblivious HTTP

1. 動作モード

2. 公開鍵

データ構造

管理方法

共有方法 (Recipient -> Sender)

共有方法 (Sender -> Recipient)

3. 暗号・復号

INFO (Application-supplied INFOrmation)

AAD (Additional Authenticated Data)

dajiajidajiaji

Use of HPKE with COSE

COSE (CBOR Object Signing and Encryption) 上でHPKEを利用するための仕様。COSEは、JWTのベース仕様としておなじみJOSE(JSON Object Signing and Encryption)のCBOR(バイナリ)版。

ここで取り上げている他の仕様と異なり、特定のユースケースにフォーカスしているわけでは無く、COSE上で汎用的にHPKEを利用する方法を規定している。

1. 動作モード

Baseモードのみ。認証はCOSEの枠組みの中で規定されているのでHPKEのものは利用しないとのこと。

2. 公開鍵

データ構造

このドラフトの中で言及されているのは、encのフォーマットのみである。COSEのCOSE_Key構造体を利用する。具体的には、COSEの仕様(RFC9153)の中で、ECDH algorithmのパラメータとしてephemeral keyがCOSE_Key型で定義されており、これを流用するのが現状の方針である。

なお、COSE_KeyはJWKのバイナリ版と考えればよい。RFC9052(Updated RFC8152)に規定されている。

COSE_Key = {
    1 => tstr / int,          ; kty
    ? 2 => bstr,              ; kid
    ? 3 => tstr / int,        ; alg
    ? 4 => [+ (tstr / int) ], ; key_ops
    ? 5 => bstr,              ; Base IV
    * label => values
}

ephemeral key (-1)の具体例は以下。

/ ephemeral / -1:{
    / kty / 1:2,
    / crv / -1:1,
    / x / -2:h'b2add44368ea6d641f9ca9af308b4079aeb519f11e9b8a55a600b21233e86e68',
    / y / -3:false
},

管理方法

共有方法 (Recipient -> Sender)

特に規定せず。Recipientの公開鍵情報にkidが含まれることは前提としている。

共有方法 (Sender -> Recipient)

COSE_Encrypt0, COSE_Encrypt(定義は以下)で運ぶ。COSE_Encrypt0/COSE_recipient構造のヘッダに、Recipientの公開鍵のkidと生成したencを入れる。

それぞれの定義は、以下の通り(RFC9152より)。

COSE_Encrypt0 = [
    Headers,
    ciphertext : bstr / nil,
]
COSE_recipient = [
    Headers,
    ciphertext : bstr / nil,
    ? recipients : [+COSE_recipient]
]
COSE_Encrypt = [
    Headers,
    ciphertext : bstr / nil,
    recipients : [+COSE_recipient]
]

なお、HPKEのパラメータのうち、kem_id と aead_idは、既存のCOSEパラメータに入れることになっている。kdf_idはどこへ?

  • kem_id: ephemeral keyのcrv値に入れる。例えば、COSE_CRV_HPKE_P256_SHA256などをIANAレジストリに登録していく模様。
  • kdf_id: ???
  • aead_id: COSE Headersのalgパラメータ。COSE_ALG_HPKE_AES_128_GCMのようなalg値をIANAにいちいち登録していく模様。

3. 暗号・復号

対象外。具体的なユースケース・アプリケーションを想定しているわけではないので。