// ==================== 設定エリア ====================// キー格納先取得設定const KEY_DIRECTORY_CONFIG = {// キャッシュを有効にするかどうか(デフォルトでは有効)enableCache: true,// オリジンサーバーのCache-Controlヘッダーに従うかどうか(デフォルトでは従います)respectCacheControl: true,// リクエストタイムアウト時間、単位:ミリ秒(デフォルト5秒)timeout: 5000,// デフォルトのキーディレクトリパスdefaultPath: '/.well-known/http-message-signatures-directory'};// ==================== 定数 ====================// HTTPメッセージ署名ディレクトリのメディアタイプ(RFC 9421)const MEDIA_TYPE_DIRECTORY = 'application/http-message-signatures-directory+json';// ==================== エッジ関数エントリーポイント ====================addEventListener('fetch', (event) => {handleRequest(event);});async function handleRequest(event) {const request = event.request;try {// すべてのパスで署名検証が必要です。const status = await verifySignature(request, event);if (status === 'valid') {// 署名が有効なため、オリジンサーバーへ転送します。return;} else {// 署名がない、または署名が無効なリクエストを拒否します。const errorMessage = status === 'neutral'? 'Missing signature headers': status.replace('invalid: ', '');return event.respondWith(new Response(JSON.stringify({error: 'authentication_failed',message: errorMessage,timestamp: new Date().toISOString()}), {status: 401,headers: {'Content-Type': 'application/json','Cache-Control': 'no-cache, no-store, must-revalidate'}}));}} catch (error) {console.error(`[handleRequest] ${error.stack}`);return event.respondWith(new Response(JSON.stringify({error: 'internal_server_error',message: error.message || 'An internal error occurred',timestamp: new Date().toISOString()}), {status: 500,headers: { 'Content-Type': 'application/json' }}));}}// ==================== 署名検証のコアロジック ====================/*** リクエスト署名を検証します。* @param {Request} request - リクエストオブジェクト* @returns {Promise<string>} 検証ステータス: 'valid' | 'neutral' | 'invalid: <reason>'*/async function verifySignature(request, event) {try {// 署名ヘッダーがないため、'neutral'を返します。if (!request.headers.has('Signature')) {return 'neutral';}// 署名リクエストヘッダーを取得します。const signature = request.headers.get('Signature');const signatureInput = request.headers.get('Signature-Input');if (!signature || !signatureInput) {return 'invalid: Missing signature headers';}// Signature-Inputリクエストヘッダーを解析します。const parsedSignatureInput = parseSignatureInput(signatureInput);if (!!parsedSignatureInput.error) {return `invalid: ${parsedSignatureInput.error}`;}}const parsedSignature = parseSignature(signature, parsedSignatureInput.signatureId);if (!!parsedSignature.error) {return `invalid: ${parsedSignature.error}`;};const signatureBytes = parsedSignature.bytes;// パラメータを検証します。const signatureParamsValidation = validateSignatureParams(parsedSignatureInput.params);if (!!signatureParamsValidation.error) {return `invalid: ${signatureParamsValidation.error}`;}// キー格納先URLを取得します。const keyDirectoryUrlResult = getKeyDirectoryUrl(request);if (keyDirectoryUrlResult.error) {return `invalid: ${keyDirectoryUrlResult.error}`;}const keyDirectoryUrl = keyDirectoryUrlResult.url;// キー格納先データを取得し、予期されるkeyidを渡して整合性検証を行います。const keyDirectoryResult = await fetchKeyDirectory(keyDirectoryUrl, event, parsedSignatureInput.params.keyid);if (keyDirectoryResult.error) {return `invalid: ${keyDirectoryResult.error}`;}const keyDirectory = keyDirectoryResult.directory;const verifiedKeyId = keyDirectoryResult.verifiedKeyId;// 公開鍵を検索します(検証済みのkeyidを使用)。const jwk = findPublicKey(keyDirectory.keys, verifiedKeyId);if (!jwk) {return `invalid: Public key not found for keyid: ${verifiedKeyId}`;}// 署名ベース文字列を構築します。const signatureBase = buildSignatureBase(request, parsedSignatureInput.components, parsedSignatureInput.signatureParams);console.log(`[verifySignature] signatureBase: ${signatureBase}`);// Ed25519署名を検証します。const [isValid, reason] = await verifyEd25519(signatureBase, signatureBytes, jwk);if (isValid) {return 'valid';} else {return !!reason ? `invalid: ${reason}` : 'invalid: Signature verification failed';}} catch (error) {console.error(`[verifySignature] ${error.stack}`);return `invalid: ${error.message}`;}}/*** リクエストヘッダーからキー格納先URLを抽出します。* @param {Request} request - リクエストオブジェクト* @returns {{ error?: string; url?: string }} 解析結果*/function getKeyDirectoryUrl(request) {try {const signatureAgent = request.headers.get('Signature-Agent');if (!signatureAgent) {return { error: 'Missing Signature-Agent header' };}// 値は二重引用符で囲む必要があります。if (!signatureAgent.startsWith('"') || !signatureAgent.endsWith('"')) {return { error: 'Signature-Agent header value must be enclosed in double quotes' };}// 二重引用符を削除します。const urlValue = signatureAgent.slice(1, -1);// HTTPSプロトコルである必要があります。if (!urlValue.startsWith('https://')) {return { error: 'Signature-Agent header value must be a valid HTTPS URL' };}// URL形式を検証します。let url;try {url = new URL(urlValue);} catch (error) {return { error: `Invalid URL in Signature-Agent header: ${error.message}` };}// URLが正しいキー格納先パスを指していることを確認します。// URLがデフォルトパスで終わっていない場合、自動的に追加します。if (!url.pathname.endsWith(KEY_DIRECTORY_CONFIG.defaultPath)) {// パスがデフォルトパスでない場合、ドメイン名のルートパスであるかどうかを確認します。if (url.pathname === '/' || url.pathname === '') {url.pathname = KEY_DIRECTORY_CONFIG.defaultPath;} else {// それ以外の場合は元のパスを維持しますが、警告を記録します。console.warn(`[getKeyDirectoryUrl] URL path may not be standard key directory: ${url.pathname}`);}}return { url: url.toString() };} catch (error) {console.error(`[getKeyDirectoryUrl] ${error.stack}`);return { error: `Failed to extract key directory URL: ${error.message}` };}}/*** キー格納先データを取得し、キャッシュに対応します。* @param {string} url - キー格納先URL* @param {any} event - イベントオブジェクト。キャッシュ書き込みに使用されます。* @param {string|null} expectedKeyId - 期待されるkeyid。応答の整合性を検証するために使用されます(任意)。* @returns {Promise<{ error?: string; directory?: { keys: Array }, verifiedKeyId?: string }>} 結果を取得します。検証済みのkeyidが含まれます。*/async function fetchKeyDirectory(url, event, expectedKeyId = null) {try {const cache = caches.default;const cacheKey = new Request(url, { eo: { cacheKey: url } });// キャッシュが有効な場合、まずキャッシュからの読み取りを試みます。if (KEY_DIRECTORY_CONFIG.enableCache) {const cachedResponse = await cache.match(cacheKey);if (cachedResponse) {console.log(`[fetchKeyDirectory] Cache hit for ${url}`);try {// キャッシュレスポンスに必要なSignature-Inputヘッダーが含まれているかどうかを検証します。const cachedSignatureInput = cachedResponse.headers.get('Signature-Input');if (!cachedSignatureInput) {console.warn(`[fetchKeyDirectory] Cached response missing Signature-Input header, skipping cache`);// キャッシュをスキップし、ネットワーク取得を続行します。} else {// レスポンスヘッダーのkeyidパラメータを解析します。const parsedCachedInput = parseSignatureInput(cachedSignatureInput);if (parsedCachedInput.error) {console.warn(`[fetchKeyDirectory] Failed to parse cached Signature-Input header: ${parsedCachedInput.error}, skipping cache`);} else {const cachedKeyId = parsedCachedInput.params.keyid;if (!cachedKeyId) {console.warn(`[fetchKeyDirectory] Cached response missing keyid parameter, skipping cache`);} else if (expectedKeyId !== null && cachedKeyId !== expectedKeyId) {console.warn(`[fetchKeyDirectory] Cached response keyid mismatch: expected ${expectedKeyId}, got ${cachedKeyId}, skipping cache`);} else {// キャッシュの検証に成功しました。ディレクトリと検証済みのkeyidを返します。const directory = await cachedResponse.json();return { directory, verifiedKeyId: cachedKeyId };}}}} catch (error) {console.error(`[fetchKeyDirectory] Failed to process cached response: ${error.message}`);// キャッシュ処理に失敗したため、ネットワーク取得を続行します。}}}// リクエストオプションを構築し、タイムアウトを設定します。const fetchOptions = {redirect: 'manual',eo: {timeoutSetting: {connectTimeout: KEY_DIRECTORY_CONFIG.timeout,readTimeout: KEY_DIRECTORY_CONFIG.timeout,writeTimeout: KEY_DIRECTORY_CONFIG.timeout}}};// リクエストを開始します。const response = await fetch(url, fetchOptions);if (!response.ok) {return { error: `HTTP ${response.status} fetching key directory from ${url}` };}// Content-Typeを検証します。const contentType = response.headers.get('Content-Type');if (!contentType || !contentType.includes(MEDIA_TYPE_DIRECTORY)) {return { error: `Invalid Content-Type: ${contentType}, expected ${MEDIA_TYPE_DIRECTORY}` };}// Signature-Inputレスポンスヘッダーを検証します。const responseSignatureInput = response.headers.get('Signature-Input');if (!responseSignatureInput) {return { error: 'Key directory response missing required Signature-Input header' };}// レスポンスヘッダーのkeyidパラメータを解析します。const parsedResponseInput = parseSignatureInput(responseSignatureInput);if (parsedResponseInput.error) {return { error: `Failed to parse response Signature-Input header: ${parsedResponseInput.error}` };}const responseKeyId = parsedResponseInput.params.keyid;if (!responseKeyId) {return { error: 'Key directory response Signature-Input missing keyid parameter' };}// 予期されるkeyidが提供されている場合、整合性を検証します。if (expectedKeyId !== null && responseKeyId !== expectedKeyId) {return { error: `Key directory response keyid mismatch: expected ${expectedKeyId}, got ${responseKeyId}` };}// レスポンスボディを解析します。let directory;try {if (KEY_DIRECTORY_CONFIG.enableCache) {directory = await response.clone().json();} else {directory = await response.json();}} catch (error) {return { error: `Failed to parse key directory response as JSON: ${error.message}` };}// ディレクトリ構造を検証します。if (!directory || !Array.isArray(directory.keys)) {return { error: 'Invalid key directory structure: missing or invalid "keys" array' };}// キャッシュが有効で、かつレスポンスがキャッシュ可能な場合、キャッシュに書き込みます。if (KEY_DIRECTORY_CONFIG.enableCache) {// オリジンサーバーのCache-Controlに従う必要があるかどうかを確認します。let shouldCache = true;if (KEY_DIRECTORY_CONFIG.respectCacheControl) {const cacheControl = response.headers.get('Cache-Control');if (cacheControl && cacheControl.includes('no-store')) {shouldCache = false;}}if (shouldCache) {event.waitUntil(cache.put(cacheKey, response.clone()));console.log(`[fetchKeyDirectory] Cached response for ${url}`);}}return { directory, verifiedKeyId: responseKeyId };} catch (error) {console.error(`[fetchKeyDirectory] Error fetching ${url}:`, error);return { error: `Failed to fetch key directory: ${error.message}` };}}/*** Signature-Inputリクエストヘッダーを解析します。* @param {string} signatureInput - Signature-Inputリクエストヘッダー値。* @returns {{ error?: string; signatureId: string; components: string[]; params: Record<string, string|number>; signatureParams: string;}} 解析結果。*/function parseSignatureInput(signatureInput) {try {const match = signatureInput.match(/^(\\w+)=(.+)$/);if (!match) {return { error: 'Failed to parse Signature-Input - invalid Signature-Input format' };}const [, signatureId, paramString] = match;// コンポーネントリストを解析します。const componentsMatch = paramString.match(/^\\(([^)]+)\\)/);if (!componentsMatch) {return { error: 'Failed to parse Signature-Input - missing components list' };}const componentsStr = componentsMatch[1];const components = componentsStr.split(/\\s+/).map(comp => comp.replace(/"/g, ''));// パラメータを解析します。const paramsStr = paramString.substring(componentsMatch[0].length);const params = {};const paramRegex = /;(\\w+)=([^;]+)/g;let paramMatch;while ((paramMatch = paramRegex.exec(paramsStr)) !== null) {const [, key, value] = paramMatch;if (value.startsWith('"') && value.endsWith('"')) {params[key] = value.slice(1, -1);} else if (/^\\d+$/.test(value)) {params[key] = parseInt(value, 10);} else {params[key] = value;}}// パラメータ文字列を再構築します。const signatureParams = `(${components.map(c => `"${c}"`).join(' ')})` +Object.entries(params).map(([key, value]) => {return typeof value === 'string' ? `;${key}="${value}"` : `;${key}=${value}`;}).join('');return { signatureId, components, params, signatureParams };} catch (error) {console.error(`[parseSignatureInput] ${error.stack}`);return { error: `Failed to parse Signature-Input: ${error.message}` }}}/*** Signatureリクエストヘッダーを解析します。* @param {string} signature - Signatureリクエストヘッダー値。* @param {string} signatureId - 署名ID。* @returns {{ error?: string; bytes: Uint8Array }} 解析結果。*/function parseSignature(signature, signatureId) {try {const pattern = new RegExp(`${signatureId}=:([A-Za-z0-9+/=_-]+):`);const match = signature.match(pattern);if (!match) {return { error: `Failed to parse Signature - signature for ${signatureId} not found` };}let signatureB64 = match[1];// base64urlを標準のbase64に変換します。signatureB64 = signatureB64.replace(/-/g, '+').replace(/_/g, '/');while (signatureB64.length % 4 !== 0) {signatureB64 += '=';}// バイト配列にデコードします。const binaryString = atob(signatureB64);const bytes = new Uint8Array(binaryString.length);for (let i = 0; i < binaryString.length; i++) {bytes[i] = binaryString.charCodeAt(i);}return { bytes };} catch (error) {console.error(`[parseSignature] ${error.stack}`);return { error: `Failed to parse Signature: ${error.message}` };}}/*** 署名パラメータを検証します。* @param {Record<string, string|number>} params - 署名パラメータ。* @returns {{ error?: string }} 検証結果。*/function validateSignatureParams(params) {console.log(`[validateSignatureParams] ${JSON.stringify(params)}`);try {if (!params.keyid) {return { error: 'Failed to validate signature parameters - missing keyid parameter' };}if (!params.alg || params.alg !== 'ed25519') {return { error: 'Failed to validate signature parameters - invalid or missing algorithm parameter' };}if (!params.tag || params.tag !== 'web-bot-auth') {return { error: 'Failed to validate signature parameters - invalid or missing tag parameter' };}if (!params.created || !params.expires) {return { error: 'Failed to validate signature parameters - missing created or expires parameter' };}const now = Math.floor(Date.now() / 1000);const clockSkew = 300; // 5分間のクロックスキュー許容値。if (params.created > now + clockSkew) {return { error: 'Failed to validate signature parameters - created time is in the future' };}if (params.expires < now - clockSkew) {return { error: 'Failed to validate signature parameters - signature has expired' };}const maxAge = 3600; // 1時間の最大有効期限。if (params.expires - params.created > maxAge) {return { error: 'Failed to validate signature parameters - signature validity period too long' };}return {};} catch (error) {console.error(`[validateSignatureParams] ${error.stack}`);return { error: `Failed to validate signature parameters - ${error.message}` };}}/*** 公開鍵を検索します。* @param {Array} keys - キー格納先の公開鍵配列。* @param {string} keyid - 公開鍵ID。* @returns {{ kid: string; kty: string; crv: string; x: string; }|undefined} 公開鍵。*/function findPublicKey(keys, keyid) {for (const key of keys) {if (key.kid === keyid) {return key;}}return null;}/*** 署名ベース文字列を構築します。* @param {Request} request - リクエストオブジェクト。* @param {string[]} components - コンポーネントリスト。* @param {string} signatureParams - 署名パラメータ。* @returns {string} 署名ベース文字列。*/function buildSignatureBase(request, components, signatureParams) {const lines = [];for (const component of components) {const value = extractComponent(request, component);lines.push(`"${component.toLowerCase()}": ${value}`);}lines.push(`"@signature-params": ${signatureParams}`);return lines.join('\\n');}/*** Ed25519署名検証。* @param {string} signatureBase - 署名ベース文字列。* @param {Uint8Array} signature - 署名バイト配列。* @param {{ kid: string; kty: string; crv: string; x: string; }} jwk - 公開鍵JWK。* @returns {Promise<[boolean, string]>} 検証結果、[検証成功かどうか、エラーメッセージ]。*/async function verifyEd25519(signatureBase, signature, jwk) {try {// JWK形式を検証します。if (!jwk || jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519') {return [false, 'Failed to verify Ed25519 signature - invalid JWK format'];}// 署名の長さを検証します。if (!signature || signature.length !== 64) {return [false, `Failed to verify Ed25519 signature - invalid signature length (${signature?.length || 'null'}), expected 64`];}// 公開鍵をデコードします。const publicKeyBytes = base64UrlDecode(jwk.x);if (publicKeyBytes.length !== 32) {return [false, `Failed to verify Ed25519 signature - invalid public key length (${publicKeyBytes.length}), expected 32`];}// メッセージをエンコードします。const message = new TextEncoder().encode(signatureBase);const result = await _verifyEd25519(signature, message, publicKeyBytes);return [result];} catch (error) {console.error(`[verifyEd25519] ${error.stack}`);return [false, `Failed to verify Ed25519 signature - ${error.message}`];}}/*** Ed25519検証のコアロジック。* @param {Uint8Array} signature - 64バイトの署名。* @param {Uint8Array} message - メッセージ。* @param {Uint8Array} publicKey - 32バイト公開鍵。* @returns {Promise<boolean>} 検証結果。*/async function _verifyEd25519(signature, message, publicKey) {if (signature.length !== 64) throw new Error('Signature must be 64 bytes');if (publicKey.length !== 32) throw new Error('Public key must be 32 bytes');try {const POINT_G = Point.BASE;// 公開鍵ポイントAを解析します。const A = Point.fromBytes(publicKey);// 署名のR部分を解析します。const R = Point.fromBytes(signature.slice(0, 32));// 署名のS部分を解析します。const S = Ed25519.bytesToNumLE(signature.slice(32, 64));if (S >= Ed25519.N) throw new Error('S out of range');// k = H(R || A || M)を計算します。const hashable = concatBytes(R.toBytes(), A.toBytes(), message);const hashed = await sha512(hashable);const k = Ed25519.mod(Ed25519.bytesToNumLE(hashed), Ed25519.N);// 等式を検証します:[8][S]B = [8]R + [8][k]A。// 以下と同等です:([8]R + [8][k]A) - [8][S]B = 0。const SB = POINT_G.multiply(S);const kA = A.multiply(k);const RkA = R.add(kA);// RkA - SBを計算して補因子を除去し、ゼロ点かどうかを確認します。const diff = RkA.add(SB.negate()).clearCofactor();return diff.isZero();} catch (error) {console.error(`[Ed25519] ${error.stack}`);return false;}}// ==================== 補助関数 ====================。/*** コンポーネント値を抽出します。* @param {Request} request - リクエストオブジェクト。* @param {string} component - コンポーネント。* @returns {string} コンポーネント値。*/function extractComponent(request, component) {if (component.startsWith('@')) {// 派生コンポーネント。const url = new URL(request.url);switch (component) {case '@method':return request.method.toUpperCase();case '@authority':return url.host;case '@scheme':return url.protocol.slice(0, -1);case '@target-uri':return request.url;case '@request-target':return `${url.pathname}${url.search}`;case '@path':return url.pathname;case '@query':return url.search.slice(1);default:throw new Error(`Failed to extract component - component ${component} is not supported`);}} else {// HTTPヘッダー。const value = request.headers.get(component.toLowerCase());return value || '';}}/*** Base64URLデコード。* @param {string} str - Base64URLエンコードされた文字列。* @returns {Uint8Array} デコードされたバイト配列。*/function base64UrlDecode(str) {// base64urlを標準のbase64に変換します。let base64 = str.replace(/-/g, '+').replace(/_/g, '/');const padding = base64.length % 4;if (padding) {base64 += '='.repeat(4 - padding);}// デコード。const binaryString = atob(base64);const bytes = new Uint8Array(binaryString.length);for (let i = 0; i < binaryString.length; i++) {bytes[i] = binaryString.charCodeAt(i);}return bytes;}// SHA-512ハッシュ。async function sha512(message) {const msgBuffer = message instanceof Uint8Array ? message : new Uint8Array(message);const hashBuffer = await crypto.subtle.digest('SHA-512', msgBuffer);return new Uint8Array(hashBuffer);}// バイト配列を連結します。function concatBytes(...arrays) {const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);const result = new Uint8Array(totalLength);let offset = 0;for (const arr of arrays) {result.set(arr, offset);offset += arr.length;}return result;}// ==================== 補助クラス ====================。// Ed25519ユーティリティクラス - すべての曲線パラメータと数学関数をカプセル化します。class Ed25519 {// Ed25519曲線パラメータ(静的プロパティ)。static P = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed');static N = BigInt('0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed');static Gx = BigInt('0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a');static Gy = BigInt('0x6666666666666666666666666666666666666666666666666666666666666658');static D = BigInt('0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3');static RM1 = BigInt('0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0');static B256 = BigInt(2) ** BigInt(256);// モジュロ演算。static mod(a, b = Ed25519.P) {const r = a % b;return r >= 0n ? r : b + r;}// モジュラ逆数。static invert(num, md = Ed25519.P) {if (num === 0n || md <= 0n) throw new Error('Invalid inverse');let a = Ed25519.mod(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;while (a !== 0n) {const q = b / a, r = b % a;const m = x - u * q, n = y - v * q;b = a, a = r, x = u, y = v, u = m, v = n;}if (b !== 1n) throw new Error('No inverse');return Ed25519.mod(x, md);}// バイトを大整数に変換します(リトルエンディアン)。static bytesToNumLE(bytes) {let result = 0n;for (let i = 0; i < bytes.length; i++) {result += BigInt(bytes[i]) << (8n * BigInt(i));}return result;}// 大整数をバイトに変換します(リトルエンディアン)。static numTo32bLE(num) {const bytes = new Uint8Array(32);let n = num;for (let i = 0; i < 32; i++) {bytes[i] = Number(n & 0xFFn);n >>= 8n;}return bytes;}// pow2(x, k) = x^(2^k) mod Pstatic pow2(x, power) {let r = x;while (power-- > 0n) {r = (r * r) % Ed25519.P;}return r;}// (p+3)/8乗を計算します。static pow_2_252_3(x) {const x2 = (x * x) % Ed25519.P;const b2 = (x2 * x) % Ed25519.P;const b4 = (Ed25519.pow2(b2, 2n) * b2) % Ed25519.P;const b5 = (Ed25519.pow2(b4, 1n) * x) % Ed25519.P;const b10 = (Ed25519.pow2(b5, 5n) * b5) % Ed25519.P;const b20 = (Ed25519.pow2(b10, 10n) * b10) % Ed25519.P;const b40 = (Ed25519.pow2(b20, 20n) * b20) % Ed25519.P;const b80 = (Ed25519.pow2(b40, 40n) * b40) % Ed25519.P;const b160 = (Ed25519.pow2(b80, 80n) * b80) % Ed25519.P;const b240 = (Ed25519.pow2(b160, 80n) * b80) % Ed25519.P;const b250 = (Ed25519.pow2(b240, 10n) * b10) % Ed25519.P;const pow_p_5_8 = (Ed25519.pow2(b250, 2n) * x) % Ed25519.P;return { pow_p_5_8, b2 };}// 平方根の計算。static uvRatio(u, v) {const v3 = Ed25519.mod(v * v * v);const v7 = Ed25519.mod(v3 * v3 * v);const pow = Ed25519.pow_2_252_3(u * v7).pow_p_5_8;let x = Ed25519.mod(u * v3 * pow);const vx2 = Ed25519.mod(v * x * x);const root1 = x;const root2 = Ed25519.mod(x * Ed25519.RM1);const useRoot1 = vx2 === u;const useRoot2 = vx2 === Ed25519.mod(-u);const noRoot = vx2 === Ed25519.mod(-u * Ed25519.RM1);if (useRoot1) x = root1;if (useRoot2 || noRoot) x = root2;if ((Ed25519.mod(x) & 1n) === 1n) x = Ed25519.mod(-x);return { isValid: useRoot1 || useRoot2, value: x };}}// 座標点クラスを拡張します。class Point {constructor(X, Y, Z, T) {this.X = X;this.Y = Y;this.Z = Z;this.T = T;}}static fromBytes(bytes) {if (bytes.length !== 32) throw new Error('Invalid point bytes length');const normed = new Uint8Array(bytes);const lastByte = bytes[31];normed[31] = lastByte & ~0x80;const y = Ed25519.bytesToNumLE(normed);if (y >= Ed25519.P) throw new Error('Y coordinate out of range');const y2 = Ed25519.mod(y * y);const u = Ed25519.mod(y2 - 1n);const v = Ed25519.mod(Ed25519.D * y2 + 1n);let { isValid, value: x } = Ed25519.uvRatio(u, v);if (!isValid) throw new Error('Invalid point: y is not a square root');const isXOdd = (x & 1n) === 1n;const isLastByteOdd = (lastByte & 0x80) !== 0;if (isLastByteOdd !== isXOdd) x = Ed25519.mod(-x);return new Point(x, y, 1n, Ed25519.mod(x * y));}// 点の加算。add(other) {const A = Ed25519.mod(this.X * other.X);const B = Ed25519.mod(this.Y * other.Y);const C = Ed25519.mod(this.T * Ed25519.D * other.T);const D2 = Ed25519.mod(this.Z * other.Z);const E = Ed25519.mod((this.X + this.Y) * (other.X + other.Y) - A - B);const F = Ed25519.mod(D2 - C);const G = Ed25519.mod(D2 + C);const H = Ed25519.mod(B + A);const X3 = Ed25519.mod(E * F);const Y3 = Ed25519.mod(G * H);const T3 = Ed25519.mod(E * H);const Z3 = Ed25519.mod(F * G);return new Point(X3, Y3, Z3, T3);}// 点のスカラー倍。double() {const A = Ed25519.mod(this.X * this.X);const B = Ed25519.mod(this.Y * this.Y);const C = Ed25519.mod(2n * Ed25519.mod(this.Z * this.Z));const D = Ed25519.mod(-A);const E = Ed25519.mod((this.X + this.Y) * (this.X + this.Y) - A - B);const G = D + B;const F = G - C;const H = D - B;const X3 = Ed25519.mod(E * F);const Y3 = Ed25519.mod(G * H);const T3 = Ed25519.mod(E * H);const Z3 = Ed25519.mod(F * G);return new Point(X3, Y3, Z3, T3);}// スカラー乗算。multiply(scalar) {const POINT_ZERO = Point.ZERO;let p = POINT_ZERO;let d = this;let n = scalar;while (n > 0n) {if (n & 1n) p = p.add(d);d = d.double();n >>= 1n;}return p;}// 負の値を取得します。negate() {return new Point(Ed25519.mod(-this.X), this.Y, this.Z, Ed25519.mod(-this.T));}// ゼロ点かどうかを判定します。isZero() {return this.X === 0n && this.Y === this.Z;}// 補因子をクリアします。clearCofactor() {return this.double().double().double();}// バイトに変換します。toBytes() {const iz = Ed25519.invert(this.Z);const x = Ed25519.mod(this.X * iz);const y = Ed25519.mod(this.Y * iz);const bytes = Ed25519.numTo32bLE(y);bytes[31] |= (x & 1n) ? 0x80 : 0;return bytes;}// 基点(遅延初期化)。static get BASE() {if (!this._BASE) {this._BASE = new Point(Ed25519.Gx, Ed25519.Gy, 1n, Ed25519.mod(Ed25519.Gx * Ed25519.Gy));}return this._BASE;}// 零点(遅延初期化)。static get ZERO() {if (!this._ZERO) {this._ZERO = new Point(0n, 1n, 1n, 0n);}return this._ZERO;}}


kty、crv、x の3つの必須フィールドを含める必要があり、またキー識別子 kid として JWK フィンガープリント(Thumbprint)を計算する必要があります。Content-Type の値は application/http-message-signatures-directory+json である必要があります。Signature-Input を必ず含める必要があり、その形式は以下の通りです:sig1=("@method" "@target-uri" "@authority");alg="ed25519";keyid="<kid>";tag="web-bot-auth";created=<timestamp>;expires=<timestamp>
kidは公開鍵の識別子です。{"keys": [{"kid": "公開鍵の識別子、オプション","kty": "OKP","crv": "Ed25519","x": "公開鍵のx座標(Base64URLエンコード)"}]}
Signature、Signature-Input、および Signature-Agent を追加する必要があります。コンポーネント | 説明 | サポート可否 |
@method | HTTPリクエストメソッド | 対応 |
@target-uri | 完全なリクエストURI(RFC 9421標準) | 対応 |
@authority | リクエストホスト | 対応 |
@scheme | プロトコル(HTTP / HTTPS) | 対応 |
@request-target | リクエストパス+クエリパラメータ | 対応 |
@path | リクエストパス | 対応 |
@query | クエリパラメータ | 対応 |
@query-param | 単一クエリパラメータ | 非対応 |
content-digest | リクエストボディのダイジェスト | 非対応 |
フィードバック