TLS鍵導出の新しい選択肢:Node.js exportKeyingMaterial() の詳細解説
主な役割と用途
このメソッドの主な目的は、TLS でネゴシエートされたセッションキーに基づいて、他のセキュリティプロトコルや機能で使用するための派生鍵(derived key)を生成することです。例えば、以下のような用途が考えられます。
- セキュアなストレージ
TLS で保護されたデータの暗号化に使用された鍵を、後で安全に保存するために、その鍵マテリアルを派生させて利用できます。 - カスタム認証
TLS 接続が確立された後、さらに別の認証メカニズムを追加する場合に、TLS の鍵マテリアルをその認証の基礎として利用できます。 - SRTP (Secure Real-time Transport Protocol)
音声やビデオなどのリアルタイムデータを安全に転送するために使用されるプロトコルで、TLS で確立された鍵から派生した鍵を利用できます。
メソッドの構文
tlsSocket.exportKeyingMaterial(length, context)
context
(Buffer | TypedArray | DataView | null): 鍵マテリアルの派生に使用されるコンテキスト文字列またはバッファです。これは、同じ TLS 接続から異なる目的で鍵を派生させる場合に、それぞれの鍵が衝突しないようにするために使用されます。特定のコンテキストがない場合はnull
を指定できます。length
(number): エクスポートする鍵マテリアルのバイト数を指定します。
戻り値
このメソッドは、指定された length
のバイト数を持つ Buffer
オブジェクトを返します。このバッファには、派生された鍵マテリアルが含まれています。
重要な点
- エクスポートされた鍵マテリアルの安全性は、基となる TLS 接続の安全性に依存します。TLS 接続が適切に確立され、強力な暗号スイートが使用されていることを確認する必要があります。
context
パラメータは、鍵の派生において重要な役割を果たします。異なるcontext
を使用することで、同じ TLS セッションからでも異なる鍵を生成できます。これにより、それぞれの鍵を特定の目的に限定することができます。tlsSocket
が完全に確立された TLS 接続を表している場合にのみ、このメソッドを呼び出すことができます。接続が確立される前に呼び出した場合、エラーが発生する可能性があります。
例
例えば、SRTP で使用するための鍵マテリアルをエクスポートする場合、以下のようなコードになる可能性があります。
const tls = require('tls');
const fs = require('fs');
const options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
requestCert: true,
rejectUnauthorized: false,
};
const server = tls.createServer(options, (tlsSocket) => {
tlsSocket.on('secureConnect', () => {
if (tlsSocket.authorized) {
console.log('クライアントは認証されました。');
const srtpKeyLength = 20; // SRTP の鍵長
const srtpContext = Buffer.from('SRTP Keying Material', 'utf8');
const srtpKeyMaterial = tlsSocket.exportKeyingMaterial(srtpKeyLength, srtpContext);
console.log('SRTP 鍵マテリアル:', srtpKeyMaterial.toString('hex'));
tlsSocket.end('安全な接続を終了します。');
} else {
console.log('クライアントの認証に失敗しました。');
tlsSocket.destroy();
}
});
});
server.listen(8000, () => {
console.log('TLS サーバーがポート 8000 で起動しました。');
});
// クライアント側のコード (例)
const client = tls.connect(8000, {
host: 'localhost',
rejectUnauthorized: false,
}, () => {
console.log('TLS クライアントが接続しました。');
// クライアント側でも同様に鍵マテリアルをエクスポートできます
const srtpKeyLengthClient = 20;
const srtpContextClient = Buffer.from('SRTP Keying Material', 'utf8');
const srtpKeyMaterialClient = client.exportKeyingMaterial(srtpKeyLengthClient, srtpContextClient);
console.log('クライアント側の SRTP 鍵マテリアル:', srtpKeyMaterialClient.toString('hex'));
client.end();
});
この例では、サーバーとクライアントの両方で exportKeyingMaterial()
を使用して、SRTP で使用するための鍵マテリアルを派生させています。同じ context
を使用することで、両端で同じ鍵を生成できることが期待されます。
一般的なエラー
-
TypeError: tlsSocket.exportKeyingMaterial is not a function
:- 原因
このエラーは、使用している Node.js のバージョンがtlsSocket.exportKeyingMaterial()
をサポートしていない場合に発生します。このメソッドは TLS 1.2 以降で導入されました。 - 解決策
Node.js のバージョンを 12.0.0 以降にアップデートしてください。古いバージョンでは、この機能は利用できません。
- 原因
-
Error [ERR_SOCKET_CLOSED]: Socket has been closed
:- 原因
TLS ソケットがすでに閉じられた状態でexportKeyingMaterial()
を呼び出そうとした場合に発生します。 - 解決策
ソケットが閉じられていないことを確認してください。'close'
イベントが発生した後や、socket.destroy()
が呼び出された後には、このメソッドを呼び出すことはできません。通常は、'secureConnect'
イベントハンドラの中で呼び出すのが適切です。
- 原因
-
Error: This socket is not secured
:- 原因
exportKeyingMaterial()
は、TLS で安全に接続されたソケットに対してのみ有効です。まだ TLS ハンドシェイクが完了していない状態や、プレーンな TCP ソケットに対して呼び出した場合に発生します。 - 解決策
'secureConnect'
イベントが発生し、tlsSocket.authorized
がtrue
である(または認証が不要な設定の場合)ことを確認してからexportKeyingMaterial()
を呼び出してください。
- 原因
-
指定した
length
が不正な値の場合 (理論的にはあり得るが、通常はTypeError
や範囲外エラーになる可能性が高い):- 原因
極端に大きな値や負の値をlength
に指定した場合に、予期しない動作やエラーが発生する可能性があります。 - 解決策
必要な鍵長を正確に把握し、正の整数値を指定してください。
- 原因
-
context
に予期しない型の値を渡した場合:- 原因
context
パラメータはBuffer
,TypedArray
,DataView
, またはnull
である必要があります。それ以外の型の値を渡すと、エラーが発生する可能性があります。 - 解決策
context
に適切な型の値を渡すようにしてください。文字列を使用する場合は、Buffer.from()
などでBuffer
に変換する必要があります。
- 原因
一般的なトラブルシューティング
-
Node.js のバージョン確認
まず、使用している Node.js のバージョンを確認してください。ターミナルでnode -v
を実行することで確認できます。tlsSocket.exportKeyingMaterial()
を使用するには、バージョン 12.0.0 以降が必要です。 -
イベントのタイミング
exportKeyingMaterial()
を呼び出すタイミングが重要です。TLS 接続が完全に確立された後(通常は'secureConnect'
イベント内)に呼び出すようにしてください。それ以前に呼び出すと、ソケットがまだ安全に接続されていないためエラーになります。 -
secureConnect イベントの確認
secureConnect
イベントハンドラ内でtlsSocket.authorized
を確認し、接続が正常に確立されていることを確認してください。認証が必要な場合は、tlsSocket.authorized
がtrue
であることを確認する必要があります。 -
context の管理
異なる目的で鍵を派生させる場合は、それぞれの目的に対応した明確なcontext
を使用してください。同じcontext
を異なる目的で使用すると、セキュリティ上の問題を引き起こす可能性があります。context
は、派生する鍵の意図された用途を反映するような意味のある文字列(をBuffer
に変換したもの)を使用することが推奨されます。 -
鍵長の確認
エクスポートする鍵の長さ (length
) が、その鍵を使用するプロトコルやアルゴリズムの要件を満たしているか確認してください。不適切な長さの鍵を使用すると、セキュリティ上の問題や互換性の問題が発生する可能性があります。 -
エラーハンドリング
try...catch
ブロックを使用して、exportKeyingMaterial()
の呼び出し中に発生する可能性のあるエラーを適切に処理してください。エラーが発生した場合は、エラーメッセージをログに記録するなどして、原因を特定しやすくすることが重要です。 -
ドキュメントの参照
Node.js の公式ドキュメントでtlsSocket.exportKeyingMaterial()
の詳細な仕様や注意点を確認してください。
例1: SRTP (Secure Real-time Transport Protocol) での使用
この例では、TLS で確立された接続から SRTP で使用するための鍵マテリアルをエクスポートし、その鍵情報をコンソールに出力します。
const tls = require('tls');
const fs = require('fs');
const serverOptions = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
requestCert: false,
rejectUnauthorized: false,
};
const server = tls.createServer(serverOptions, (tlsSocket) => {
tlsSocket.on('secureConnect', () => {
if (tlsSocket.authorized || !serverOptions.requestCert) {
console.log('安全な接続が確立されました。');
const srtpKeyLength = 20; // SRTP の鍵長 (例)
const srtpContext = Buffer.from('SRTP Keying Material', 'utf8');
const srtpKeyMaterial = tlsSocket.exportKeyingMaterial(srtpKeyLength, srtpContext);
console.log('サーバー側の SRTP 鍵マテリアル:', srtpKeyMaterial.toString('hex'));
// ここで srtpKeyMaterial を SRTP の鍵として使用する処理を実装します。
tlsSocket.end('サーバーからの終了');
} else {
console.log('クライアントの認証に失敗しました。');
tlsSocket.destroy();
}
});
});
server.listen(8000, () => {
console.log('TLS サーバーがポート 8000 で起動しました。');
});
const clientOptions = {
host: 'localhost',
port: 8000,
rejectUnauthorized: false,
};
const client = tls.connect(clientOptions, () => {
console.log('TLS クライアントがサーバーに接続しました。');
const srtpKeyLengthClient = 20; // SRTP の鍵長 (例)
const srtpContextClient = Buffer.from('SRTP Keying Material', 'utf8');
const srtpKeyMaterialClient = client.exportKeyingMaterial(srtpKeyLengthClient, srtpContextClient);
console.log('クライアント側の SRTP 鍵マテリアル:', srtpKeyMaterialClient.toString('hex'));
// ここで srtpKeyMaterialClient を SRTP の鍵として使用する処理を実装します。
client.end();
});
例2: カスタム認証での使用
TLS 接続が確立した後、追加の認証ステップとして、TLS の鍵マテリアルの一部を利用するカスタム認証メカニズムを実装する例です。
const tls = require('tls');
const crypto = require('crypto');
const fs = require('fs');
const serverOptions = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
requestCert: true,
rejectUnauthorized: false,
};
const server = tls.createServer(serverOptions, (tlsSocket) => {
tlsSocket.on('secureConnect', () => {
if (tlsSocket.authorized) {
console.log('クライアントは認証されました (TLS レベル)。');
// カスタム認証用の鍵マテリアルをエクスポート
const authKeyLength = 16;
const authContext = Buffer.from('Custom Authentication Key', 'utf8');
const authKeyMaterial = tlsSocket.exportKeyingMaterial(authKeyLength, authContext);
// エクスポートされた鍵マテリアルに基づいて何らかの認証処理を行う
const expectedToken = crypto.createHmac('sha256', authKeyMaterial).update('server-secret').digest('hex');
tlsSocket.write(`AUTH_REQUIRED\n`);
tlsSocket.on('data', (data) => {
const receivedToken = data.toString().trim();
if (receivedToken === expectedToken) {
console.log('カスタム認証成功!');
tlsSocket.end('認証成功、接続を終了します。');
} else {
console.log('カスタム認証失敗!');
tlsSocket.end('認証失敗、接続を終了します。');
}
});
} else {
console.log('クライアントの TLS 認証に失敗しました。');
tlsSocket.destroy();
}
});
});
server.listen(8000, () => {
console.log('TLS サーバーがポート 8000 で起動しました。');
});
const clientOptions = {
host: 'localhost',
port: 8000,
rejectUnauthorized: false,
};
const client = tls.connect(clientOptions, () => {
console.log('TLS クライアントがサーバーに接続しました。');
client.on('data', (data) => {
const message = data.toString().trim();
if (message === 'AUTH_REQUIRED') {
// サーバーと同様にカスタム認証用の鍵マテリアルをエクスポート
const authKeyLengthClient = 16;
const authContextClient = Buffer.from('Custom Authentication Key', 'utf8');
const authKeyMaterialClient = client.exportKeyingMaterial(authKeyLengthClient, authContextClient);
// エクスポートされた鍵マテリアルに基づいてトークンを生成し、サーバーに送信
const clientToken = crypto.createHmac('sha256', authKeyMaterialClient).update('server-secret').digest('hex');
client.write(`${clientToken}\n`);
} else {
console.log('サーバーからのメッセージ:', message);
client.end();
}
});
});
この例では、TLS 接続確立後、サーバーとクライアントは同じ context
を使用してカスタム認証用の鍵マテリアルをエクスポートします。その後、この鍵マテリアルを元に生成したトークンを交換することで、TLS レベルの認証に加えて、より強固な認証を実現しています。
例3: エクスポートされた鍵マテリアルの確認
単純に exportKeyingMaterial()
がどのような出力を生成するかを確認するための例です。
const tls = require('tls');
const fs = require('fs');
const serverOptions = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
requestCert: false,
rejectUnauthorized: false,
};
const server = tls.createServer(serverOptions, (tlsSocket) => {
tlsSocket.on('secureConnect', () => {
console.log('安全な接続が確立されました。');
const keyLength = 32;
const context = Buffer.from('Test Key Material', 'utf8');
const keyMaterial = tlsSocket.exportKeyingMaterial(keyLength, context);
console.log('エクスポートされた鍵マテリアル (hex):', keyMaterial.toString('hex'));
console.log('エクスポートされた鍵マテリアル (base64):', keyMaterial.toString('base64'));
console.log('エクスポートされた鍵マテリアルの長さ:', keyMaterial.length);
tlsSocket.end('サーバーからの終了');
});
});
server.listen(8000, () => {
console.log('TLS サーバーがポート 8000 で起動しました。');
});
const clientOptions = {
host: 'localhost',
port: 8000,
rejectUnauthorized: false,
};
tls.connect(clientOptions, () => {
console.log('TLS クライアントが接続しました。');
// クライアント側でも同様に鍵マテリアルをエクスポートして確認できます
const keyLengthClient = 32;
const contextClient = Buffer.from('Test Key Material', 'utf8');
const keyMaterialClient = client.exportKeyingMaterial(keyLengthClient, contextClient);
console.log('クライアント側のエクスポートされた鍵マテリアル (hex):', keyMaterialClient.toString('hex'));
client.end();
});
この例では、エクスポートされた鍵マテリアルを 16 進数と Base64 エンコードで表示し、その長さを出力しています。これにより、exportKeyingMaterial()
が実際にバイト列を生成していることを確認できます。
TLS セッションチケット (TLS Session Tickets) の利用
- Node.js での実装
TLS サーバー/クライアントのオプションでsessionTicket
を有効にすることで利用できます。 - exportKeyingMaterial() との関連性
セッションチケット自体は直接鍵マテリアルを提供するものではありませんが、セッションを再利用することで、新しい鍵の生成を避けることができます。ある意味で、間接的にパフォーマンスの向上やリソースの節約に貢献します。
// サーバー側
const tls = require('tls');
const fs = require('fs');
const serverOptions = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
sessionTicket: true, // セッションチケットを有効にする
};
const server = tls.createServer(serverOptions, (tlsSocket) => {
// ...
});
// クライアント側
const clientOptions = {
host: 'example.com',
port: 443,
sessionTicket: true, // セッションチケットを有効にする
};
const client = tls.connect(clientOptions, () => {
// ...
});
注意点
セッションチケットは鍵漏洩のリスクがあるため、適切な鍵ローテーションなどの対策が必要です。
外部の鍵管理システム (Key Management Systems, KMS) の利用
- Node.js での実装
各 KMS プロバイダーが提供する SDK や API を利用して、Node.js アプリケーションから KMS にアクセスし、鍵の生成や取得を行います。 - exportKeyingMaterial() との関連性
TLS の鍵に依存せず、独立した鍵管理を行うため、より柔軟でセキュアな鍵管理が可能になります。
// 例: AWS KMS SDK を利用する場合
const AWS = require('aws-sdk');
const kms = new AWS.KMS();
async function generateDataKey() {
const params = {
KeyId: 'your-kms-key-id',
KeySpec: 'AES_256',
};
try {
const dataKey = await kms.generateDataKey(params).promise();
const plaintextKey = dataKey.Plaintext;
const ciphertextKeyBlob = dataKey.CiphertextBlob;
console.log('平文のデータキー (保護が必要):', plaintextKey.toString('hex'));
console.log('暗号化されたデータキー:', ciphertextKeyBlob.toString('base64'));
// ここで plaintextKey を利用し、ciphertextKeyBlob を安全に保存する
} catch (error) {
console.error('KMS エラー:', error);
}
}
generateDataKey();
独自の鍵導出関数 (Key Derivation Functions, KDF) の実装
- Node.js での実装
crypto
モジュールのハッシュ関数 (例:crypto.createHmac
,crypto.createHash
) や HKDF (HMAC-based Extract-and-Expand Key Derivation Function) などの機能を利用して、独自の KDF を実装します。 - exportKeyingMaterial() との関連性
exportKeyingMaterial()
が内部的に行っている鍵導出の処理を、より細かく制御したい場合に有効です。
const crypto = require('crypto');
function deriveKey(secret, salt, info, length) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(salt);
hmac.update(info);
const prk = hmac.digest(); // Pseudo-Random Key
const okm = crypto.hkdfSync('sha256', prk, salt, info, length); // Output Keying Material
return okm;
}
// TLS ソケットの 'secureConnect' イベント内でマスターシークレットを取得 (注意: 直接的なアクセスは推奨されません)
// これはあくまで概念的な例です。TLS の内部構造に深く依存するため、安定性やセキュリティに注意が必要です。
// 通常、マスターシークレットを直接取得する API は提供されていません。
// const masterSecret = tlsSocket.masterSecret; // このような API は通常ありません
// 代わりに、TLS ネゴシエーションに関する何らかの共有情報や秘密情報に基づいて導出することを検討します。
const sharedSecret = Buffer.from('some-shared-secret', 'utf8');
const salt = Buffer.from('unique-salt', 'utf8');
const info = Buffer.from('application-specific-info', 'utf8');
const derivedKey = deriveKey(sharedSecret, salt, info, 32);
console.log('導出された鍵:', derivedKey.toString('hex'));
注意点
独自の KDF を実装する場合は、暗号学的な知識が必要であり、セキュリティ上のリスクを十分に理解する必要があります。
- Node.js での実装
SRTP であればnode-srtp
などの専用のライブラリを利用します。 - exportKeyingMaterial() との関連性
TLS の鍵導出機能を利用するのではなく、各プロトコルの標準的な鍵交換や鍵導出のメカニズムに従います。
// 例: node-srtp ライブラリを利用する場合
const srtp = require('node-srtp');
async function setupSrtp() {
try {
const localSrtpSession = await srtp.createSession({
localSsrc: 12345,
srtp_profile: 'SRTP_AES128_CM_HMAC_SHA1_80',
localCryptoKey: Buffer.from('thisisourlocalkeythatis30byteslong', 'hex'),
});
const remoteSrtpSession = await srtp.createSession({
remoteSsrc: 67890,
srtp_profile: 'SRTP_AES128_CM_HMAC_SHA1_80',
remoteCryptoKey: Buffer.from('thisisourremotekeythatis30byteslong', 'hex'),
});
// ... SRTP セッションを利用したメディアの送受信処理 ...
} catch (error) {
console.error('SRTP エラー:', error);
}
}
setupSrtp();