ããã«ã¡ã¯ã24 åã§å ¥ç¤¾ãã7 æãããã¹ã¿ãã£ãµã㪠for SCHOOLã ã®éçºã«æºãã£ã¦ãã @nagasho1122 ã§ãã æ¬è¨äºã¯ã¹ã¿ãã£ãµããªProduct Team Advent Calendar 2024 21æ¥ç®ã®è¨äºã«ãªãã¾ãã
æ¬æ¥ã¯ãè² è·è©¦é¨ãããã©ã¼ãã³ã¹ãã¹ãã«å©ç¨ãããk6ãç¨ãã¦ãåå¦çãTypeScriptã§è¨è¿°ããRSAå ¬ééµãç¨ãããã£ã¬ã³ã¸ã¬ã¹ãã³ã¹èªè¨¼ã®APIãå¼ã³åºãæ¹æ³ã«ã¤ãã¦ç´¹ä»è´ãã¾ãï¼
k6ã¨ã¯
k6ã¯ãGrafana Labsããªã¼ãã³ã½ã¼ã¹ã¨ãã¦æä¾ããã³ã¼ããã¼ã¹ã®è² è·è©¦é¨ãã¼ã«ã§ãã ãã¹ãã·ããªãªã¯JavaScriptã§è¨è¿°ã§ããæä¾ããã¦ããã¡ã½ããã¯ã·ã³ãã«ã§ç´æçã§ããCLIä¸ã§ç°¡åã«å®è¡ã§ããã ãã§ãªãããã©ã°ã¤ã³ãæ¡å¼µæ©è½ãæ´»ç¨ãã¦ã«ã¹ã¿ãã¤ãºãããã¨ãå¯è½ã§ãã
k6ã®ç¹å¾´
k6ã§ã¯ãåVirtual Userï¼VUï¼åä½ã§åé¢ãããç¬èªã®JavaScript Runtimeãå®è¡ããã¾ãã ããã¯ãNode.jsããã©ã¦ã¶ã§å©ç¨ããã¦ããV8ãJSCã¨ã¯ç°ãªãã¾ãã ããã«ä¼´ããNode.jsãæä¾ããAPIä¾åã®ããã±ã¼ã¸ãwindowãªãã¸ã§ã¯ãã®ãããªãã©ã¦ã¶åºæã®APIã¯åä½ããªãã¨ããå¶ç´ããããk6ã«å¯¾å¿ããã©ã¤ãã©ãªã使ç¨ããå¿ è¦ãããã¾ãã
TypeScriptãå©ç¨ããçç±
2024å¹´6æã«ãªãªã¼ã¹ãããk6ã®v0.52.0以éãTypeScriptãç´æ¥å®è¡å¯è½ã«ãªãã¾ããã APIã®ãªã¯ã¨ã¹ãããã£ãè¤éãªå ´åãTypeScriptã®åå®ç¾©ãé常ã«å½¹ç«ã¤ãããç§ã¯TypeScriptã®ä½¿ç¨ãé¸æãã¦ãã¾ãã
k6ãTypeScriptã§å®è¡ããå¨ãã®è©±ã¯ãTypeScriptã¦ã è² è·ãã¹ããæ¸ãã ãk6ã®ã·ã³ã¯ãã«ããã¤ããªã®ç§å¯ãããåãããããã§ãã
ãããããã¨
k6ãå©ç¨ãã¦è² è·ãã¹ããè¡ãéãèªè¨¼ãå«ãAPIãå¼ã³åºããã¨ãããã¾ãã ãã®ã¨ããèªè¨¼ãçªç ´ããå¿ è¦ããããç¹ã«èªè¨¼ã«ããåã¦ã¼ã¶ã¼ãèå¥ããAPIã®å ´åãVUãã¨ã«é©åãªèªè¨¼å¦çãæ±ãããã¾ãã ä»åã®ã·ããªãªã§ã¯ãVUãã¨ã«RSAå ¬ééµãç¨ãããã£ã¬ã³ã¸ã¬ã¹ãã³ã¹èªè¨¼ãçªç ´ããå¿ è¦ãããã¾ããã
k6ã§ã®æå·æä½
k6ã§ã¯ãWebCrypto APIãk6ä¸ã§å®è¡ã§ããããã«æ¡å¼µããç¬èªã®WebCrypto APIãå©ç¨å¯è½ã§ãã ãã®APIãç¨ãããã¨ã§ãk6ä¸ã§æå·åã»å¾©å·åããã¸ã¿ã«ç½²åã®çæã»æ¤è¨¼ãéµã®çæã»ç®¡çãªã©ã®æå·æä½ãè¡ããã¨ãã§ãã¾ãã
ãã ããæ¬APIã¯ã¾ã å®é¨çãªã¢ã¸ã¥ã¼ã«ã§ãããWebCrypto APIã«åå¨ããä¸é¨ã®ã¢ã«ã´ãªãºã ãæ©è½ãä¸è¶³ãã¦ããå¯è½æ§ãããã¾ãã ç¹ã«ãRSAæ¹å¼ã®éµãå©ç¨ããå¦çã¯ã2024å¹´11æã®k6ã®v0.55.0ãªãªã¼ã¹ã§å®å ¨ã«ãµãã¼ãããã¾ããã ããã«ãããWebAuthnãPasskeyãªã©ã®èªè¨¼ããã¼ã§RSAæ¹å¼ã®éµãå©ç¨ãããã¼ãºã«å¿ãããã¨ãã§ãããããã®æè¡ãç¨ããã»ãã¥ã¢ãªèªè¨¼ããã»ã¹ãk6ã§ãã¹ãã§ããããã«ãªãã¾ããã
k6ã§ã®åºæ¬çãªæå·æä½ææ³ã¯ãk6ã§ã®æå·æä½ããåãããããã§ãã
èªè¨¼æé
ãã¼ãã¢ãçæ
å
¬ééµãç¨ãããã£ã¬ã³ã¸ã¬ã¹ãã³ã¹èªè¨¼ã§ã¯ãã¦ã¼ã¶ã¼ã®ããã¤ã¹å´ã§ãã¼ãã¢ãçæããå¿
è¦ããããããk6å´ã§RSAæ¹å¼ã®éµãã¢ãçæãã¾ãã
以ä¸ã®generateKeyPair
ã¡ã½ããã§ã¯ãcrypto.subtle.generateKey
ã¡ã½ããã使ç¨ãã¦ãéµãã¢ãçæãã¦ãã¾ãã
- 第ä¸å¼æ°ã«ã¯ãçæããéµã®ç¨®é¡ãããã·ã¥æ¹å¼ãæå®ãã¾ãã
- nameã«ã¯RSASSA-PKCS1-v1_5ãæå®ããRSAç½²åæ¹å¼ãé¸æãã¾ãã
- modulusLengthã«ã¯
2048
ããããè¨å®ãã¦ãã¾ããä»ã«ã1024
ã4096
çã使ç¨å¯è½ã§ãã - publicExponentã«ã¯ã
65537
ã®24ããã表ç¾ãæå®ãã¦ãã¾ãã - hashã«ã¯
SHA-256
ãæå®ãã¦ãã¾ãã
- 第äºå¼æ°ã¯ãçæããéµãã¨ã¯ã¹ãã¼ãå¯è½ãã©ããã示ããã¼ã«å¤ã§ããtrueãæå®ãããã¨ã§ãå¾ã§éµãã¨ã¯ã¹ãã¼ãã§ããããã«ãã¦ãã¾ãã
- 第ä¸å¼æ°ã«ã¯ãçæããéµãã©ã®ããã«ä½¿ç¨ããããã示ãæååã®é
åã渡ãã¾ããããã§ã¯ãç½²åãè¡ãããã«
["sign", "verify"]
ãæå®ãã¦ãã¾ãã
åã¢ã«ã´ãªãºã ã¨ç¬¬ä¸å¼æ°ã®çµã¿åããã¯ããã¥ã¡ã³ããã確èªãã ããã
import { crypto, CryptoKeyPair } from "k6/experimental/webcrypto"; export async function generateKeyPair(): Promise<CryptoKeyPair> { const keyPair = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, true, ["sign", "verify"], ); return keyPair; }
å ¬ééµãPEMå½¢å¼ã«å¤æãã¦éä¿¡
ä½æãããã¼ãã¢ã®å
¬ééµã¯ãcrypto.subtle.exportKey
ã¡ã½ããã使ç¨ãã¦ArrayBufferã¨ãã¦ã¨ã¯ã¹ãã¼ãã§ãã¾ãã
getPublicKeyPem
ã¡ã½ããã«ã¦ããã®ã¨ã¯ã¹ãã¼ããããå
¬ééµãBase64ã¨ã³ã³ã¼ãããPEMå½¢å¼ã«å¤æãã¦APIã®ãªã¯ã¨ã¹ãããã£ã«æ ¼ç´ãã¾ãã
import encoding from "k6/encoding"; import { crypto, CryptoKey } from "k6/experimental/webcrypto"; export async function getPublicKeyPem(publicKey: any) { const publicKeyBuffer = await exportPublicKey(publicKey); const publicKeyBase64 = encoding.b64encode(new Uint8Array(publicKeyBuffer)); return toPEM(publicKeyBase64, "PUBLIC KEY"); } async function exportPublicKey(publicKey: CryptoKey): Promise<ArrayBuffer> { return await crypto.subtle.exportKey("spki", publicKey) as ArrayBuffer; // spkiãæå®ãã¦ããã®ã§ãArrayBufferãè¿ã } function toPEM(base64: string, type: string): string { const pemHeader = `-----BEGIN ${type}-----\n`; const pemFooter = `-----END ${type}-----\n`; const pemBody = base64.match(/.{1,64}/g)?.join("\n") || ""; return pemHeader + pemBody + "\n" + pemFooter; }
ãã£ã¬ã³ã¸ããç½²åãçæ
PEMå½¢å¼ã®å
¬ééµãAPIã«POSTãããã¨ã§ãã£ã¬ã³ã¸ã¨å¼ã°ããä¹±æ°ãè¿ã£ã¦ãã¾ãã
èªè¨¼ããAPIã®ä»æ§ä¸ããã£ã¬ã³ã¸ã¯UTF-8ã¨ã³ã³ã¼ãã£ã³ã°ããå¿
è¦ããã£ããããencodeUTF8Character
ã¡ã½ããã«ã¦ã¨ã³ã³ã¼ããã¦ãã¾ãã
k6å´ã§crypto.subtle.sign
ã¡ã½ããã«ã¦ç§å¯éµãç¨ãã¦ãã£ã¬ã³ã¸ããç½²åãçæãããã¨ãã§ãã¾ãã
- 第ä¸å¼æ°: çæããéµã®ç¨®é¡ãããã·ã¥æ¹å¼ãæå®ãã¾ãã
- nameã«ã¯RSASSA-PKCS1-v1_5ãæå®ããRSAç½²åæ¹å¼ãé¸æãã¾ãã
- modulusLengthã«ã¯2048ããããè¨å®ããã»ãã¥ãªãã£ã®è¦³ç¹ããé©åãªéµãµã¤ãºã確ä¿ãã¾ãã
- publicExponentã«ã¯ã65537ã®24ããã表ç¾ãæå®ãã¾ãã
- hashã«ã¯SHA-256ãæå®ããç½²åã®ããã·ã¥ã¢ã«ã´ãªãºã ãè¨å®ãã¾ãã
- githubä¸ã®ãµã³ãã«ã³ã¼ãã«è¨è¼ããã¦ããRSAã®ã¢ã«ã´ãªãºã ã¯ã2024å¹´12æç¾å¨ã®k6å
¬å¼ã®åå®ç¾©ã«å¯¾å¿ãã¦ããªãã£ããããåã¨ã©ã¼ãç¡è¦ããããã«
// @ts-ignore
ã使ç¨ãã¦ãã¾ãã
- 第äºå¼æ°: çæããéµãã¨ã¯ã¹ãã¼ãå¯è½ãã©ããã示ããã¼ã«å¤ã§ããtrueãæå®ãããã¨ã§ãå¾ã§éµãã¨ã¯ã¹ãã¼ãã§ããããã«ãªãã¾ãã
- 第ä¸å¼æ°: çæããéµãã©ã®ããã«ä½¿ç¨ããããã示ãæååã®é åã渡ãã¾ããããã§ã¯ãç½²åãè¡ãããã«["sign", "verify"]ãæå®ãã¦ãã¾ãã
ä½æããç½²åã¯ãªã¯ã¨ã¹ããããã¼ã«æ¹è¡ãå«ãããã¨ãåºæ¥ãªãããã RFC4648 "Base 64 Encoding with URL and Filename Safe Alphabet" ã§ã¨ã³ã³ã¼ããã¦ãã¾ãã
import encoding from "k6/encoding"; import { crypto, CryptoKey } from "k6/experimental/webcrypto"; const SIGN_ALGORITHM = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }; async function getUrlSafeSignedText( privateKey: any, text: string, ): Promise<string> { const signature = await signText(privateKey, text); const signatureBase64 = encoding.b64encode(new Uint8Array(signature)); return toUrlSafeBase64(signatureBase64); } async function signText( privateKey: CryptoKey, text: string, ): Promise<ArrayBuffer> { return await crypto.subtle.sign( // @ts-ignore SIGN_ALGORITHM, privateKey, stringToArrayBuffer(text), ); } const stringToArrayBuffer = (str: string): Uint8Array => { const utf8 = []; for (let i = 0; i < str.length; i++) { utf8.push(...encodeUTF8Character(str.charCodeAt(i))); } return new Uint8Array(utf8); }; function encodeUTF8Character(code: number): number[] { const utf8Char = []; if (code < 0x80) { utf8Char.push(code); } else if (code < 0x800) { utf8Char.push((code >> 6) | 0xc0, (code & 0x3f) | 0x80); } else if (code < 0x10000) { utf8Char.push( (code >> 12) | 0xe0, ((code >> 6) & 0x3f) | 0x80, (code & 0x3f) | 0x80, ); } else if (code < 0x110000) { utf8Char.push( (code >> 18) | 0xf0, ((code >> 12) & 0x3f) | 0x80, ((code >> 6) & 0x3f) | 0x80, (code & 0x3f) | 0x80, ); } return utf8Char; } function toUrlSafeBase64(base64: string): string { return base64.replace(/\+/g, "-").replace(/\//g, "_"); }
ã¾ã¨ã
æçµçã«ä»¥ä¸ã®ãããªã³ã¼ãã«ãªãã¾ããï¼
import encoding from "k6/encoding"; import { crypto, CryptoKey, CryptoKeyPair } from "k6/experimental/webcrypto"; const SIGN_ALGORITHM = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } }; export async function generateKeyPair(): Promise<CryptoKeyPair> { const keyPair = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, true, ["sign", "verify"], ); return keyPair; } export async function getUrlSafeSignedText( privateKey: any, text: string, ): Promise<string> { const signature = await signText(privateKey, text); const signatureBase64 = encoding.b64encode(new Uint8Array(signature)); return toUrlSafeBase64(signatureBase64); } async function signText( privateKey: CryptoKey, text: string, ): Promise<ArrayBuffer> { return await crypto.subtle.sign( // @ts-ignore SIGN_ALGORITHM, privateKey, stringToArrayBuffer(text), ); } export async function getPublicKeyPem(publicKey: any) { const publicKeyBuffer = await exportPublicKey(publicKey); const publicKeyBase64 = encoding.b64encode(new Uint8Array(publicKeyBuffer)); return toPEM(publicKeyBase64, "PUBLIC KEY"); } async function exportPublicKey(publicKey: CryptoKey): Promise<ArrayBuffer> { return await crypto.subtle.exportKey("spki", publicKey) as ArrayBuffer; // spkiãæå®ãã¦ããã®ã§ãArrayBufferãè¿ã } function toPEM(base64: string, type: string): string { const pemHeader = `-----BEGIN ${type}-----\n`; const pemFooter = `-----END ${type}-----\n`; const pemBody = base64.match(/.{1,64}/g)?.join("\n") || ""; return pemHeader + pemBody + "\n" + pemFooter; } const stringToArrayBuffer = (str: string): Uint8Array => { const utf8 = []; for (let i = 0; i < str.length; i++) { utf8.push(...encodeUTF8Character(str.charCodeAt(i))); } return new Uint8Array(utf8); }; function encodeUTF8Character(code: number): number[] { const utf8Char = []; if (code < 0x80) { utf8Char.push(code); } else if (code < 0x800) { utf8Char.push((code >> 6) | 0xc0, (code & 0x3f) | 0x80); } else if (code < 0x10000) { utf8Char.push( (code >> 12) | 0xe0, ((code >> 6) & 0x3f) | 0x80, (code & 0x3f) | 0x80, ); } else if (code < 0x110000) { utf8Char.push( (code >> 18) | 0xf0, ((code >> 12) & 0x3f) | 0x80, ((code >> 6) & 0x3f) | 0x80, (code & 0x3f) | 0x80, ); } return utf8Char; } function toUrlSafeBase64(base64: string): string { return base64.replace(/\+/g, "-").replace(/\//g, "_"); }
åVUãã¨ã«ã以ä¸ã®å·¥ç¨ãè¸ããã¨ã§ãVUãã¨ã«ç°ãªãã¦ã¼ã¶ã¼ã¨ãã¦è² è·è©¦é¨ãè¡ããã¨ãã§ãã¾ãã
- generateKeyPairã§éµãçæ
- getPublicKeyPemã§PEMå½¢å¼ã®éµãåå¾
- ãã£ã¬ã³ã¸ã»ã¬ã¹ãã³ã¹èªè¨¼ã®APIã«PEMå½¢å¼ã®éµãPOST
- APIãããã£ã¬ã³ã¸ãè¿ã£ã¦ãã
- getUrlSafeSignedTextã«ã¦ããã£ã¬ã³ã¸ãç§å¯éµãç¨ãã¦ç½²åããURL safeãªå½¢å¼ã«ã¨ã³ã³ã¼ã
- ç½²åãHeaderã«æ ¼ç´ãã¦APIãå©ããã¨ã§èªè¨¼çªç ´
æå¾ã«
以ä¸ãk6ã¨TypeScriptãç¨ããRSAå ¬ééµãç¨ãããã£ã¬ã³ã¸ã¬ã¹ãã³ã¹èªè¨¼ã®å®è£ ã«ã¤ãã¦ç´¹ä»ãã¾ããã ç§ãæ¬å®è£ ããããã¨ããåæ¥ã«k6 v0.55.0ããªãªã¼ã¹ãããRSAã¸ã®å®å ¨å¯¾å¿ããªããããããé常ã«ã¿ã¤ãã³ã°ãè¯ãã£ãã¨æãã¦ãã¾ãï¼
ææ¥ã®ããã°ãã楽ãã¿ã«ï¼