コース目次 / 第2章

署名と検証 — 改ざんを止めているもの

node:crypto でHS256のJWTを自分で作り、検証します。ペイロードを書き換えると検証が落ちること、正しく再署名するには鍵が要ることを、手元で確かめます。

第2章 / 全5章目安 約11分この章のゴール: JWTの署名の仕組みを理解し、改ざんが検証で検知される様子を確かめる

中身が誰でも読めるなら、書き換えてしまえばいいのでは?——たとえば role を user から admin に。それを止めているのが、3つめの部品、署名です。この章は自分でトークンを作り、署名の働きを手元で確かめます。ここからは node を使います(標準の node:crypto だけ。外部ライブラリは要りません)。

法と倫理ゲート

手を動かす前に

このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。

  • (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
  • (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
  • (c) CTF 競技の中(規約の範囲で)

実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。

自分で、JWT を作る

HS256(HMAC-SHA256)という、最も基本的な方式でトークンを1本作ります。make-jwt.js として保存して実行します。

// make-jwt.js — 自分用の JWT を1本作る(HS256)。標準の node:crypto だけ。
const crypto = require('node:crypto');

const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
const secret = 'my-demo-secret'; // 本来はサーバだけが知る鍵

const header = { alg: 'HS256', typ: 'JWT' };
const payload = { user: 'haruto', role: 'user' };

const data = b64(header) + '.' + b64(payload);
// 署名 = 「ヘッダ.ペイロード」を、鍵で HMAC-SHA256 した値。
const sig = crypto.createHmac('sha256', secret).update(data).digest('base64url');
const token = data + '.' + sig;

console.log(token);
node make-jwt.js

3部品がピリオドでつながった、あなたのトークンが出ます。署名は「ヘッダ.ペイロード」を鍵で HMAC した値——つまり、鍵を知らないと、正しい署名は作れません。ここが要点です。

検証する

サーバは、届いたトークンをこう確かめます:「ヘッダとペイロードから、自分の鍵で署名を計算し直して、届いた署名と一致するか」。verify-jwt.js として、作ったトークンを引数に渡します。

// verify-jwt.js — 届いたトークンの署名を検証する。
const crypto = require('node:crypto');
const token = process.argv[2];
const secret = 'my-demo-secret';

const [h, p, s] = token.split('.');
const expected = crypto.createHmac('sha256', secret).update(h + '.' + p).digest('base64url');

if (s === expected) {
  console.log('署名OK。中身:', Buffer.from(p, 'base64url').toString());
} else {
  console.log('署名NG。改ざんされたか、鍵が違う。');
}
node verify-jwt.js "<さっき作ったトークン>"

署名OK と、ペイロードの中身が出ます。正しく作ったトークンは、正しく通ります。

改ざんを、試す

では、攻撃者の気持ちで role を admin に書き換えたトークンを作り、それを検証にかけます。署名は元のまま(鍵を知らないので作り直せない、という想定)。

// tamper.js — ペイロードだけ admin に書き換え、署名は元のまま付ける。
const token = process.argv[2];
const [h, , s] = token.split('.');
const evilPayload = Buffer.from(JSON.stringify({ user: 'haruto', role: 'admin' })).toString('base64url');
console.log(h + '.' + evilPayload + '.' + s); // 署名は古いまま
node tamper.js "<元のトークン>"        # 改ざんトークンが出る
node verify-jwt.js "<改ざんトークン>"   # これを検証にかける

結果は 署名NG。ペイロードを書き換えた瞬間、「ヘッダ.ペイロード」が変わるので、正しい署名の値も変わります。でも攻撃者は鍵を知らないから、新しい正しい署名を作れない。だから古い署名は合わなくなり、検証で弾かれる。
これが JWT の守りの芯です——中身は誰でも読めるが、書き換えると署名が合わなくなる。そして正しい署名を作るには、サーバだけが持つ鍵が要る。署名だけが、改ざんを止めています。

だからこそ、次章が怖い

ここまでで、正しく実装された JWT が改ざんに強いことが分かりました。裏を返すと——この「署名の検証」を甘くする実装があれば、改ざんが通ってしまう。攻撃者が狙うのは、まさにそこです。

署名だけが砦なら、攻撃は「署名の検証をどうやって迂回するか」に集中します。署名が無くても通してしまう設定、鍵の種類を取り違えさせる手口、鍵が弱くて当てられるケース。次の章で、この3つを自作トークンで見ます。

持ち帰る一言

署名だけが、改ざんを止めている。 HS256 の署名は「ヘッダ.ペイロードを鍵で HMAC した値」で、鍵を知らないと作れない。だからペイロードを書き換えると検証が落ちる。この唯一の砦を甘くする実装が、次章の攻撃対象です。

こうなっていればOK

卒業まであと2章です。

この章はまだ完了していません。