コース目次 / 第2章
署名と検証 — 改ざんを止めているもの
node:crypto でHS256の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章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。