1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

jsonwebtokenからjoseへ移行する際の PEM 形式問題と解決策

Posted at

はじめに

Node.js プロジェクトで JWT (JSON Web Token) の検証を行う際、多くの開発者が従来の jsonwebtoken ライブラリから、よりモダンな jose ライブラリへの移行を検討します。しかし、この移行プロセス中に、公開鍵のフォーマットが互換性のない問題に遭遇することがよくあります。この記事では、私が移行中に直面した問題と、その最終的な解決策について共有します。


結論:crypto モジュールで PEM 形式を統一する

TL;DR: jsonwebtoken から jose に移行する際、 Invalid keyData または wrong tag エラーが発生した場合、Node.js の crypto モジュールを使用して PEM 形式を標準化する必要があります。

import * as crypto from 'crypto';
import * as jose from 'jose';

// crypto を使用して PEM 形式を標準化
const key = crypto.createPublicKey(ssoPublicKeyPem);
const fixedPem = key.export({
  type: 'spki',
  format: 'pem'
});

// 標準化された PEM を使用して jose で検証
const publicKey = await jose.importSPKI(fixedPem, 'RS256');
const { payload } = await jose.jwtVerify(token, publicKey);

少し苦戦したデバッグ

従来の jsonwebtoken による実装

当初、私は jsonwebtoken ライブラリを使って JWT 検証を行っていました。そのコードは非常にシンプルでした。

import jwt from 'jsonwebtoken';

const ssoPublicKey = process.env.SSO_PUBLIC_KEY.replace(/\\n/g, '\n') || '';
const payload = jwt.verify(token, ssoPublicKey);
console.log('JWT 検証成功、Payload:', payload);

この実装は問題なく動作していました。

jose への移行時に発生したエラー

コードを jose ライブラリに移行しようとした際、最初の実装は以下のようになりました。

import * as jose from 'jose';

const ssoPublicKeyPem = process.env.SSO_PUBLIC_KEY.replace(/\\n/g, '\n') || '';
const publicKey = await jose.importSPKI(ssoPublicKeyPem, 'RS256');

しかし、実行時に以下のエラーが発生しました。

DOMException [DataError]: Invalid keyData
    at Object.rsaImportKey (node:internal/crypto/rsa:221:15)
    at SubtleCrypto.importKey (node:internal/crypto/webcrypto:615:10)
    ...
    [cause]: Error: error:068000A8:asn1 encoding routines::wrong tag

PEM のヘッダ/フッタの変更を試みる

エラーメッセージを見て、まず PEM 形式の問題を疑いました。環境変数内の公開鍵が以下のようになっていることに気づきました。

SSO_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n...

そこで、ヘッダとフッタを RSA PUBLIC KEY から PUBLIC KEY に変更することを試みました。

// ヘッダ/フッタ形式の変更を試みる
let ssoPublicKeyPem = process.env.SSO_PUBLIC_KEY.replace(/\\n/g, '\n') || '';
ssoPublicKeyPem = ssoPublicKeyPem
  .replace('-----BEGIN RSA PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----')
  .replace('-----END RSA PUBLIC KEY-----', '-----END PUBLIC KEY-----');

しかし、このように変更しても同じエラーが発生しました。


2つのライブラリの内部処理メカニズムの差異

jsonwebtoken の処理方法

jsonwebtoken ライブラリは内部で、より緩やかな鍵解析メカニズムを使用しています。これにより、以下のことが可能です。

  • 様々な PEM 形式(RSA PUBLIC KEYPUBLIC KEY など)を自動的に処理
  • フォーマットエラーに対する一定の許容性
  • 内部でいくつかの形式変換と標準化を実行

jose ライブラリの処理方法

一方、jose ライブラリはより厳格です。

  • Web Crypto API 標準に厳密に従って鍵を解析
  • PEM 形式が標準の SPKI 形式(-----BEGIN PUBLIC KEY-----)であることを要求
  • ASN.1 エンコーディングに対してより厳格な要件
  • Node.js の Web Crypto API を直接呼び出し

なぜ crypto モジュールが必要なのか

問題の根本原因は、PEM のヘッダ/フッタを手動で変更したとしても、内部の ASN.1 エンコーディング構造jose ライブラリの要件を満たしていない可能性がある点にあります。

Node.js の crypto.createPublicKey() メソッドは、以下のことを実現します。

  1. 様々な形式の公開鍵を自動的に識別RSA PUBLIC KEYPUBLIC KEY など)
  2. 内部エンコーディング構造を標準化し、ASN.1 エンコーディングが標準に準拠していることを保証
  3. 標準の SPKI 形式を出力。これこそが jose.importSPKI() が必要とするものです。
// crypto.createPublicKey の処理プロセス:
// 1. 元の PEM を解析(どんな形式でも)
// 2. 公開鍵の数学的パラメータを抽出
// 3. 標準の SPKI 形式に再エンコード
// 4. 標準の PEM 文字列を出力

const key = crypto.createPublicKey(originalPem);
const standardizedPem = key.export({
  type: 'spki', // SPKI 形式での出力を指定
  format: 'pem' // PEM 文字列での出力を指定
});

まとめ

修正後の jose による実装

import dotenv from 'dotenv';
dotenv.config({ path: '../.env' }); // .env ファイルのパスは適宜調整してください

import * as jose from 'jose';
import * as crypto from 'crypto';

// 環境変数内の公開鍵を処理
const ssoPublicKeyPem = process.env.SSO_PUBLIC_KEY.replace(/\\n/g, '\n') || '';
console.log(`debug:ssoPublicKeyPem: ${ssoPublicKeyPem}`);

// Node.js の crypto モジュールを使用して PEM 形式を標準化
const key = crypto.createPublicKey(ssoPublicKeyPem);
const fixedPem = key.export({
  type: 'spki',
  format: 'pem'
});
console.log(`debug:fixedPem: ${fixedPem}`);

// JWT トークン検証 (例として dummy token を使用)
// 実際のアプリケーションでは検証対象の JWT を渡してください
const token = 'YOUR_JWT_TOKEN_HERE'; // ここに検証したい JWT を入れます

async function verifyToken() {
  try {
    // 標準化された PEM 形式から公開鍵をインポート
    const publicKey = await jose.importSPKI(fixedPem, 'RS256');

    // JWT を検証
    const { payload, protectedHeader } = await jose.jwtVerify(token, publicKey);

    console.log('JWT 検証成功、Protected Header:', protectedHeader);
    console.log('JWT 検証成功、Payload:', payload);

    return payload;
  } catch (error) {
    console.error('JWT 検証失敗:', error);
    throw error;
  }
}

// 関数の呼び出し (テスト用)
verifyToken();

jsonwebtoken から jose へ移行する際、重要なのは、両ライブラリの公開鍵形式に対する要求の違いを理解することです。jose ライブラリはより厳格で標準化されており、これによって多少の複雑性が増しますが、その分、パフォーマンス向上とより厳格なセキュリティを実現します。

crypto.createPublicKey() を用いた形式の標準化は、公開鍵形式の互換性を確保し、移行プロセスをよりスムーズにするための確実な解決策です。


参考資料


あとがき

この Qiita 記事が、jsonwebtoken から jose への移行でお困りの方々の一助となれば幸いです。初めて技術的な記事を書くので、本当にこのテーマで大丈夫か自信がないです。もし疑問点があれば、お気軽にコメントください。

ちなみに、この記事の本文はGitHub Copilotを使って書いてもらったものです。私がデバッグする際、Google検索に加えて、Copilotにも問題解決の手助けをしてもらったからです。Copilotは、私が遭遇したエラーも記録してくれて、指定したアウトラインに沿って自動的に.mdファイルに書き込むことができます。出来上がった記事を自分で読み直し、不要だと感じた比較表の一部を削除したほかは、ほとんど手を加えていません。本当に助かりました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?