- Go 1.22 เปลี่ยนแหล่งเลขสุ่มเริ่มต้นของ
math/randและmath/rand/v2ให้เป็น ตัวสร้างที่แข็งแกร่งเชิงวิทยาการเข้ารหัสลับ เพื่อลดความเสียหายอย่างมากเมื่อใช้ผิดในจุดที่ควรใช้crypto/rand - ตัวสร้างเดิมของ Go 1 เป็น linear feedback shift register ที่ใช้สถานะ
uint64จำนวน 607 ค่า ดังนั้นเพียงสังเกตผลลัพธ์ 607 ค่า ก็สามารถกู้คืนค่าทั้งในอดีตและอนาคตได้ - PCG-DXSM ของ
math/rand/v2ปรับปรุงคุณภาพเลขสุ่มเชิงสถิติและขนาดสถานะ แต่ไม่ได้รับประกันถึงความคาดเดาไม่ได้ที่จำเป็นสำหรับค่าลับ - ChaCha8Rand ใหม่ใช้ seed 32 ไบต์, rekey ทุก 16 บล็อก และสถานะ 300 ไบต์ต่อคอร์ โดยนำไปใช้กับ
math/rand/v2, บางส่วนของmath/randและ seed สำหรับ hash ของ map - ต้นทุนด้านประสิทธิภาพมีจำกัด: ChaCha8Rand ช้ากว่าตัวสร้างของ Go 1 แต่ไม่เกิน 2 เท่า และบนเซิร์ฟเวอร์ทั่วไปความต่างไม่เกิน 3ns จึงให้ประโยชน์ด้านความปลอดภัยมากกว่าสำหรับโปรแกรมส่วนใหญ่
ค่าเริ่มต้นของเลขสุ่มที่ Go 1.22 เปลี่ยนไป
- Go 1.22 เปลี่ยนค่าเริ่มต้นให้
math/randและmath/rand/v2ใช้ ตัวสร้างเลขสุ่มเทียมที่แข็งแกร่งเชิงวิทยาการเข้ารหัสลับ - เป้าหมายคือเพื่อลดความเสียหายเมื่อผู้พัฒนาใช้
math/randผิดในจุดที่ต้องใช้crypto/rand - API เลขสุ่มของ Go แบ่งเป็นสองหมวดมาโดยตลอด
math/rand: เลขสุ่มเชิงสถิติ ที่ใช้กับการจำลอง, sampling, numerical analysis, อัลกอริทึมสุ่มที่ไม่ใช่การเข้ารหัสลับ, fuzzing, shuffle, exponential backoff เป็นต้นcrypto/rand: เลขสุ่มเชิงวิทยาการเข้ารหัสลับ ที่ต้องมีความคาดเดาไม่ได้ เช่น key และ token
ทำไมเลขสุ่มเชิงสถิติจึงยังไม่เพียงพอ
- ตัวสร้างเลขสุ่มเชิงสถิติอาจเพียงพอสำหรับงานที่ไม่ใช่การเข้ารหัสลับหลายอย่าง หากผ่านการทดสอบเชิงสถิติพื้นฐาน
- อย่างไรก็ตาม หากผู้สังเกตที่รู้จักอัลกอริทึมเห็นผลลัพธ์มากพอ ก็มักจะคาดเดาลำดับถัดไปได้
srandและrandของ Unix V3 เป็นรูปแบบยุคแรกที่มีอิทธิพลต่อ API เลขสุ่มของ C และภาษาอื่น ๆ หลายภาษาในเวลาต่อมา- ตั้งค่าสถานะด้วย seed เป็นจำนวนเต็มค่าเดียว
- คำนวณค่าถัดไปด้วยวิธี linear congruential generator (LCG)
- สถานะภายในเรียบง่าย ทำให้คำนวณค่าในอนาคตได้ง่ายแม้เห็นผลลัพธ์เพียงค่าเดียว
- LCG สามารถเลือกค่าคงที่ให้ปล่อยค่าผลลัพธ์ที่เป็นไปได้ออกมาครั้งละหนึ่งค่าก่อนวนซ้ำได้ แต่มีจุดอ่อนคือบิตต่ำ ๆ จะวนซ้ำด้วยคาบสั้น
โครงสร้างและช่องโหว่ของตัวสร้าง Go 1
- ตัวสร้าง
math/randของ Go 1 อยู่ในตระกูล linear feedback shift register - สถานะภายในคือ slice
vecที่ประกอบด้วยuint64จำนวน 607 ค่าvec[606]คือ “tap”vec[334]คือ “feed”- เมื่อสร้างค่าถัดไป จะนำ tap และ feed มาบวกกันเป็น
xจากนั้นบันทึกxไว้ที่ตำแหน่ง feed แล้วส่งค่ากลับ
- implementation จริงไม่ได้เลื่อนทั้ง slice แต่เลื่อนเฉพาะตำแหน่ง tap และ feed ถอยหลังเพื่อลดต้นทุน
- การสร้างค่าถัดไปต้องใช้การลบสองครั้ง, การบวกแบบมีเงื่อนไขสองครั้ง, การโหลดสองครั้ง, การบวกหนึ่งครั้ง และการเก็บค่าหนึ่งครั้ง
- เพราะค่าที่ส่งกลับเป็นหนึ่งในสมาชิกของเวกเตอร์สถานะภายใน การอ่าน ผลลัพธ์ 607 ค่า จึงเปิดเผยสถานะทั้งหมด
- หากเติม
vecเดียวกันและรันอัลกอริทึม ก็สามารถคาดเดาค่าในอนาคตได้ - หากรันอัลกอริทึมย้อนกลับ ก็สามารถกู้คืนค่าในอดีตได้เช่นกัน
- หากเติม
- ตัวสร้างของ Go 1 ไม่ได้มีไว้เพื่อความปลอดภัย และคุณภาพของตัวเลขที่สร้างขึ้นก็ขึ้นกับการตั้งค่า
vecเริ่มต้น
สิ่งที่ PCG ปรับปรุงและข้อจำกัดที่ยังเหลือ
math/rand/v2นำ PCG ของ Melissa O’Neill มาใช้เป็นตัวสร้างเลขสุ่มเชิงสถิติที่ทันสมัยกว่า- PCG ของ Go อิงกับ LCG แบบ 128 บิต และใช้ฟังก์ชัน
scrambleลดสถานะ 128 บิตให้เป็นผลลัพธ์ 64 บิต - ระหว่างการอภิปรายข้อเสนอ Go ใช้
scrambleแบบอิงการคูณตามข้อเสนอของ O’Neill- รูปแบบนี้เรียกว่า PCG-DXSM
- Numpy ก็ใช้ PCG รูปแบบนี้เช่นกัน
- PCG มีสถานะเล็กกว่าตัวสร้างของ Go 1 มาก
- ตัวสร้าง Go 1:
uint64จำนวน 607 ค่า - PCG:
uint64สองค่า
- ตัวสร้าง Go 1:
- PCG ไวต่อค่าสถานะเริ่มต้นน้อยกว่าและผ่านการทดสอบเชิงสถิติหลายแบบ แต่ไม่ได้รับประกัน ความคาดเดาไม่ได้
- PCG-XSL-RR สามารถย้อนกลับได้
- PCG-DXSM แม้จะย้อนกลับได้ก็ไม่น่าแปลกใจ
- การสร้างค่าลับจำเป็นต้องใช้ตัวสร้างอื่น ไม่ใช่ PCG
เลขสุ่มเชิงวิทยาการเข้ารหัสลับและบทบาทของระบบปฏิบัติการ
- เลขสุ่มเชิงวิทยาการเข้ารหัสลับต้องคาดเดาไม่ได้ในทางปฏิบัติ แม้สำหรับผู้สังเกตที่รู้วิธีสร้างและเห็นผลลัพธ์ในอดีตจำนวนมาก
- โปรโตคอลเข้ารหัสลับ, secret key, การค้าสมัยใหม่ และความเป็นส่วนตัวออนไลน์ ล้วนพึ่งพาเลขสุ่มเชิงวิทยาการเข้ารหัสลับ
- ระบบปฏิบัติการเป็นผู้จัดหาเลขสุ่มจริง
- รวบรวมความสุ่มจากอุปกรณ์ทางกายภาพ เช่น เมาส์, คีย์บอร์ด, ดิสก์ และ timing ของเครือข่าย
- ระยะหลังยังใช้ electrical noise ที่ CPU วัดได้โดยตรงด้วย
- เมื่อระบบปฏิบัติการรวบรวมเลขสุ่มได้เพียงพอ เช่น อย่างน้อย 256 บิต ก็จะสร้างลำดับเลขสุ่มยาว ๆ ด้วย hash หรืออัลกอริทึมเข้ารหัส
- ในอดีตมักใช้ไฟล์อุปกรณ์อย่าง
/dev/randomแต่ระบบปฏิบัติการปัจจุบันมี system call โดยตรงให้ใช้งาน crypto/randของ Go ซ่อนความแตกต่างของแต่ละระบบปฏิบัติการและให้ interface เดียวกันคือrand.Read
การออกแบบ ChaCha8Rand
- ตัวสร้างใหม่ ChaCha8Rand ใน Go 1.22 เป็นรูปแบบที่ปรับเล็กน้อยจาก ChaCha stream cipher ของ Daniel J. Bernstein
- ChaCha ถูกใช้อย่างแพร่หลายในรูปแบบ ChaCha20 ซึ่งใช้ใน TLS และ SSH ด้วย
- Too Much Crypto ของ Jean-Philippe Aumasson มองว่ารูปแบบ 8 รอบอย่าง ChaCha8 ก็ปลอดภัย และ ChaCha8 เร็วกว่าประมาณ 2.5 เท่า
- เพื่อใช้ ChaCha8 เป็น
rand.SourceChaCha8Rand ไม่ XOR บล็อกที่สร้างกับอินพุต แต่ใช้เป็นสตรีมเลขสุ่มโดยตรง- ซึ่งเทียบได้กับการเข้ารหัสหรือถอดรหัสข้อมูลที่เป็นศูนย์ทั้งหมด
จุดเปลี่ยนของ ChaCha8Rand
- ChaCha8Rand ใช้ seed 32 ไบต์ เป็น key ของ ChaCha8
- ChaCha8 สร้างบล็อกขนาด 64 ไบต์ และการคำนวณมองบล็อกเป็น
uint32จำนวน 16 ค่า - implementation ทั่วไปสามารถคำนวณ 4 บล็อกพร้อมกันด้วยคำสั่ง SIMD ได้ แต่หากต้องใช้กับอินพุตแบบ XOR ก็ต้องคลายบล็อก interleaved กลับมา
- ChaCha8Rand นิยามบล็อก interleaved นี้เองเป็นสตรีมเลขสุ่ม จึงตัดต้นทุน unshuffle ออกไป
- ในขั้นตอนปิดท้ายบล็อกของ ChaCha8 จะบวกค่าบางอย่างเข้ากับ
uint32แต่ละตัว- ครึ่งหนึ่งเป็น key material และอีกครึ่งเป็นค่าคงที่ที่รู้กัน
- ChaCha8Rand ไม่บวกค่าคงที่ที่รู้กันซ้ำ จึงลดการบวกสุดท้ายลงครึ่งหนึ่ง
- ทุก ๆ บล็อกที่สร้างครบ 16 บล็อก จะใช้ 32 ไบต์สุดท้ายเป็น key สำหรับ 16 บล็อกถัดไป
- การ rekey นี้ให้ forward secrecy รูปแบบหนึ่ง
- แม้สถานะทั้งหมดในหน่วยความจำของตัวสร้างจะรั่วไหล ก็จะกู้คืนได้เฉพาะค่าหลังการ rekey ล่าสุด และเข้าถึงค่าในอดีตไม่ได้
- Go เผยแพร่ สเปก ChaCha8Rand C2SP และชุดทดสอบ เพื่อให้ implementation อื่นสามารถมีความ repeatable ร่วมกับ implementation ของ Go สำหรับ seed เดียวกันได้
จุดที่นำไปใช้ใน standard library
- Go runtime รักษา สถานะ ChaCha8Rand ต่อคอร์ ที่ seed ด้วยเลขสุ่มเชิงวิทยาการเข้ารหัสลับจากระบบปฏิบัติการ
- ขนาดสถานะต่อคอร์คือ 300 ไบต์
- บนระบบ 16 คอร์ จะมีขนาดใกล้เคียงกับสถานะตัวสร้าง Go 1 แบบแชร์ตัวเดียวที่ 4,872 ไบต์
- สถานะแยกต่อคอร์ช่วยสร้างเลขสุ่มได้เร็วโดยไม่เกิด lock contention
- ฟังก์ชันระดับแพ็กเกจของ
math/rand/v2ใช้ ChaCha8Rand เสมอ- เช่น
rand.N,rand.Float64
- เช่น
- ฟังก์ชันระดับแพ็กเกจของ
math/randใช้ ChaCha8Rand หากไม่ได้เรียกrand.Seed- เช่น
rand.Intn,rand.Float64 - หากเรียก
rand.Seedต้องย้อนกลับไปใช้ตัวสร้าง Go 1 เพื่อความเข้ากันได้
- เช่น
- runtime เลือก seed สำหรับ hash ของ map ใหม่ด้วย ChaCha8Rand แทนตัวสร้างเดิมที่อิงกับ wyrand
- หากผู้โจมตีรู้ฟังก์ชัน hash เฉพาะของ implementation ของ map ก็สามารถเตรียมอินพุตเพื่อทำให้ map ทำงานเป็นเวลาเชิงกำลังสองได้
- การใช้ seed ต่อ map แทน seed global เพียงตัวเดียว ยังช่วยหลีกเลี่ยงพฤติกรรมเสื่อมถอยอื่น ๆ ได้ด้วย
- ยังไม่ชัดเจนว่า seed ของ map จำเป็นต้องเป็นเลขสุ่มเชิงวิทยาการเข้ารหัสลับหรือไม่ แต่การเปลี่ยนนี้ทำได้ง่ายและเป็นทางเลือกที่รอบคอบ
- โค้ดที่ต้องการอินสแตนซ์ ChaCha8Rand แยกต่างหาก สามารถสร้าง
rand.ChaCha8ได้โดยตรง
ลดความเสียหายจากความผิดพลาดด้านความปลอดภัย
- Go มีเป้าหมายช่วยให้เขียน โค้ดที่ปลอดภัยโดยปริยาย ด้วยการลดหรือกำจัดความผิดพลาดที่พบบ่อยและมีปัญหาด้านความปลอดภัย
- เมื่อ
Readของmath/randถูก deprecated ใน Go 1.20 ผู้พัฒนาบางคนพบว่าตนกำลังใช้math/randในจุดที่ควรใช้crypto/randเช่น การสร้าง key material - ใน Go 1.20 ความผิดพลาดเช่นนี้เป็นปัญหาความปลอดภัยร้ายแรง
- ต้องตรวจสอบว่า key ถูกนำไปใช้ที่ใด
- key ถูกเปิดเผยอย่างไร
- ผลลัพธ์เลขสุ่มอื่น ๆ ให้เบาะแสแก่ผู้โจมตีในการอนุมาน key หรือไม่
- ใน Go 1.22 ความผิดพลาดเดียวกันยังคงเป็นความผิดพลาด แต่โอกาสที่จะกลายเป็น หายนะด้านความปลอดภัย ลดลง
- อย่างไรก็ตาม สำหรับค่าลับ การใช้
crypto/randก็ยังดีกว่า- เคอร์เนลของระบบปฏิบัติการสามารถปกป้องค่าเลขสุ่มได้ดีกว่า
- เคอร์เนลเติม entropy ใหม่เข้าไปในตัวสร้างอย่างต่อเนื่อง
- implementation ของเคอร์เนลผ่านการตรวจสอบมากกว่า
กรณีที่ดูไม่เหมือนงานเข้ารหัสลับ
- การสร้าง UUID แบบสุ่มอาจดูเหมือนใช้
math/randก็พอ เพราะ UUID ไม่ใช่ค่าลับ - แต่หาก seed
math/randด้วยเวลาปัจจุบัน เครื่องต่าง ๆ ที่ทำงานในเวลาเดียวกันอาจสร้างค่าเดียวกันได้- บนระบบที่เวลาปัจจุบันมีความละเอียดแค่ระดับมิลลิวินาที ความเป็นไปได้นี้ยิ่งมากขึ้น
- แม้ Go 1.20 จะมี auto seeding ที่อิง entropy จาก OS แต่ seed ของตัวสร้าง Go 1 เป็นเพียงจำนวนเต็ม 63 บิต
- โปรแกรมที่สร้าง UUID ตอนเริ่มทำงานจะมี UUID แรกที่เป็นไปได้จำกัดอยู่ที่ 2⁶³ ค่า
- หลังมี UUID ประมาณ 2³¹ ค่า ก็เริ่มมีโอกาสชนกัน
- ChaCha8Rand ของ Go 1.22 ถูก seed ด้วย entropy 256 บิต
- UUID แรกที่เป็นไปได้มี 2²⁵⁶ ค่า
- ไม่ต้องกังวลเรื่องการชนกัน
- การทำ load balancing ที่ frontend server สุ่มกระจายคำขอไปยัง backend server ก็อาจต้องใช้เลขสุ่มที่คาดเดาไม่ได้
- หากผู้โจมตีสังเกตการจัดสรรและรู้จักอัลกอริทึมที่คาดเดาได้ ก็สามารถเทคำขอราคาแพงไปยัง backend เฉพาะเครื่องได้
- ในตัวสร้าง Go 1 ปัญหานี้พบได้ยากแต่เป็นไปได้
- ใน Go 1.22 ไม่เป็นปัญหา
ลักษณะด้านประสิทธิภาพ
- ประโยชน์ด้านความปลอดภัยของ ChaCha8Rand มีต้นทุนเล็กน้อย แต่ประสิทธิภาพอยู่ในช่วงเดียวกับตัวสร้าง Go 1 และ PCG
- operation ที่นำมาเทียบมีสองแบบ
Uint64: ส่งคืนuint64ถัดไปจากสตรีมเลขสุ่มN(1000): ส่งคืนเลขสุ่มในช่วง[0, 1000)
- เมื่อ build ด้วย
GOARCH=386บนชิป x86 64 บิตและรันในโหมด 32 บิต PCG จะช้ากว่า ChaCha8Rand เพราะการคูณ 128 บิตของ PCG- ChaCha8Rand ใช้เลขคณิต SIMD แบบ 32 บิต
- ในบางระบบ
Go 1: Uint64เร็วกว่าPCG: Uint64แต่Go 1: N(1000)ช้ากว่าPCG: N(1000)N(1000)ของ Go 1 ใช้การหารจำนวนเต็ม 64 บิตสองครั้งเพื่อลดช่วงN(1000)ของ PCG และ ChaCha8 ใช้อัลกอริทึมของmath/rand/v2ที่เร็วกว่าและหลีกเลี่ยงการหารในกรณีส่วนใหญ่
- โดยรวม ChaCha8Rand ช้ากว่าตัวสร้าง Go 1 แต่ ไม่ช้าลงเกิน 2 เท่า
- บนเซิร์ฟเวอร์ทั่วไป ความต่างไม่เกิน 3ns และมีโปรแกรมน้อยมากที่ความต่างนี้จะกลายเป็นคอขวด
สรุป
- Go 1.22 เพิ่มความปลอดภัยให้โปรแกรมโดยไม่ต้องแก้โค้ด
- แนวทางหลักคือเสริมความแข็งแกร่งให้
math/randเอง เพื่อลดปัญหาที่พบบ่อยจากการใช้math/randผิดแทนcrypto/rand - มีกรณีอย่างแพ็กเกจ npm
keypairที่พยายามสร้างคู่กุญแจ RSA ด้วย JavaScriptMath.randomเมื่อไม่มี Web Crypto API - ความปลอดภัยของระบบไม่อาจตั้งอยู่บนสมมติฐานว่านักพัฒนาจะไม่ทำพลาด
- ChaCha8Rand ของ Go 1.22 แสดงให้เห็นว่าแนวทางการใช้ตัวสร้างเลขสุ่มเทียมที่แข็งแกร่งเชิงวิทยาการเข้ารหัสลับแม้กับเลขสุ่ม “เชิงคณิตศาสตร์” ก็สามารถให้ประสิทธิภาพที่แข่งขันกับตัวสร้างอื่นได้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
อย่างที่บทความบอกไว้ rclone เคยพลาดแบบนี้เป๊ะ
ระหว่างรีแฟกเตอร์โค้ดที่ใช้
Readของcrypto/randการ import ถูกเปลี่ยนอัตโนมัติ และน่าจะปนกับโค้ดที่ใช้math/randจนgoimportsเปลี่ยนเป็นmath/randผลก็คือแทนที่จะใช้ ตัวสร้างเลขสุ่มเชิงความปลอดภัย กลับกลายเป็นว่า rclone ใช้ ตัวสร้างแบบกำหนดผลลัพธ์ได้ ที่ seed ด้วยเวลา และไม่มีใครสังเกตจาก diff :-(
https://www.cvedetails.com/cve/CVE-2020-28924/
เพราะงั้นผมเห็นด้วยมากกับการเปลี่ยนครั้งนี้
goimportsให้เลือกcrypto/randก่อนอยู่แล้ว จึงไม่แน่ใจว่าเกิดอะไรขึ้นระหว่างการรีแฟกเตอร์อาจเป็นไปได้ว่ามีโค้ดในไฟล์เดียวกันที่ใช้ API เฉพาะของ
math/randอยู่https://go-review.googlesource.com/24847
แต่ยังไงก็ดีใจที่ได้จัดการเรื่องพวกนี้ให้เรียบร้อย
math/randทั้งที่จริงไม่ได้ใช้ แค่สับสนกับหลายไฟล์เท่านั้น โชคดีที่ไม่ใช่ปัญหาใหญ่ แต่ก็แสดงให้เห็นว่าทั้งหมดนี้ชวนสับสนแค่ไหนtext/templateกับhtml/templateก็คล้ายกัน ย้อนกลับไปมองแล้ว การ ตั้งชื่อแพ็กเกจให้ชนกันแบบนี้ เป็นไอเดียที่ไม่ดีเลย"secure password generation golang"แล้วจะเห็นว่าแทบทุกตัวอย่างใช้math/randแย่กว่านั้นคือทั้งหมดจะตั้งค่า seed ด้วยเวลาปัจจุบันทันที ก่อนสร้างรหัสผ่าน
ผมไปค้นดูหลังจากเจอว่ามีคนใช้
math/randในโค้ดของเรา แล้วสงสัยว่าก๊อปปี้มาจากไหนgoimportsมีการจัดการmath/rand.Readและcrypto/rand.Readเป็นกรณีพิเศษแทบตั้งแต่แรกแต่ถ้าดู commit ปี 2016 นี้ https://github.com/golang/tools/commit/0835c735343e0d8e375f0... จะมีการพูดถึงช่วงที่
"rand.Read"อาจถูกตีความเป็น"math/rand"ได้เป็นไปได้ว่าอาจโดนในช่วงนั้นพอดี
"PredictableRand"เมื่อสัปดาห์ก่อน spacey ก็โพสต์เรื่องนี้ไว้ที่ https://news.ycombinator.com/item?id=40237491 แต่โพสต์นั้นน่าจะถูกกลบผิด ๆ ว่าเป็นโพสต์ซ้ำของ https://news.ycombinator.com/item?id=40224864
บทความในบล็อก go.dev สองชิ้นนี้เป็นคนละตอนในซีรีส์เดียวกัน แต่ต่างกันพอสมควร โดยบทความนี้ว่าด้วย อัลกอริทึมการสร้างเลขสุ่มเชิงความปลอดภัยที่มีประสิทธิภาพ ส่วนบทความก่อนหน้านี้พูดถึง การออกแบบ API ของ Go
Russell Cox ปล่อยทั้งบล็อกเทคนิค ข้อเสนอ และผลงานดี ๆ ออกมาอย่างต่อเนื่อง
ถ้าอยากพัฒนาความชัดเจนในการเขียนและการคิด การเริ่มจาก Russell Cox ถือเป็นจุดเริ่มต้นที่ดี
ตอนนั้นผมยังไม่รู้ด้วยซ้ำว่า Russ Cox คือใคร แต่ซีรีส์นั้นยอดเยี่ยมจริง ๆ
ถ้าพูดถึงแหล่งความรู้ฟรีเรื่องการอิมพลีเมนต์ regex นี่น่าจะคุณภาพดีที่สุดแล้ว รองลงไปก็คือหนังสือแนวคอมไพเลอร์หลายเล่ม แต่พวกนั้นไม่ฟรีและก็หาอ่านบนเว็บได้ไม่ง่าย
ผมเองก็เคยใช้
math/randในจุดที่ควรต้องใช้crypto/randผลคือ dnscrypt-proxy2 เวอร์ชันแรก ๆ ใช้ คีย์แบบคงที่
สาเหตุมาจากส่วนขยาย VSCode ที่เพิ่ม import ให้อัตโนมัติ ผมระวัง import
crypto/randด้วยตัวเองในทุกไฟล์ที่ต้องใช้เลขสุ่มเชิงความปลอดภัย แต่พลาดไปไฟล์หนึ่ง ทุกอย่างคอมไพล์ได้และทำงานปกติ แล้วผมก็ไม่ทันสังเกตว่าส่วนขยายนั้นแอบเพิ่ม importmath/randให้ในไฟล์นั้นหลังจากนั้นผมก็ import
crypto/randโดยตั้งชื่อแฝงเป็นcryptorandเพื่อหลีกเลี่ยงการ auto importrandผิดตัวอีกอย่าง Zig ก็ใช้ ตัวสร้างเลขสุ่มที่อิง ChaCha8 และในการทำงานเข้ารหัส ผู้ใช้ไม่สามารถส่งตัวสร้างของตัวเองเข้าไปได้ โดยระบบจะใช้ตัวที่ปลอดภัยเสมอ ส่วนการทดสอบ ฟังก์ชันบางตัวรับ seed แบบชัดเจนได้
สำหรับสภาพแวดล้อมที่มีข้อจำกัด ใน standard library ยังมีตัวสร้างที่เล็กกว่าซึ่งอิง Ascon permutation และ Reverie construction รวมอยู่ด้วย
ในปี 2016 มีการเปลี่ยน
goimportsให้เลือกcrypto/randแทนmath/randแล้ว (https://go-review.googlesource.com/24847) และตอนนั้นก็ยังไม่มีการรองรับ Go บน VSCode ด้วยแม้จะเข้าสู่ยุค 2020 แล้ว ฉันก็ยังคิดอยู่บ่อย ๆ ว่าทำไมการติดตั้งตัวสร้างเลขสุ่มเริ่มต้นของหลายภาษาโปรแกรมยังใช้ตัวสร้างเลขสุ่มที่เร็วอย่าง LFSR หรือ MT
ดูเหมือนว่าการตั้งแบบเผื่อความปลอดภัย โดยสมมติว่าผู้คนไม่รู้ว่าตัวเองต้องการ ตัวสร้างเลขสุ่มเทียม หรือ ตัวสร้างเลขสุ่มเทียมที่ปลอดภัยเชิงเข้ารหัส แล้วเปลี่ยนค่าเริ่มต้นไปเป็นอย่างหลัง และให้เฉพาะคนที่ต้องการอย่างแรกเป็นผู้เลือกอย่างชัดเจน จะดีกว่า
ถ้านักพัฒนาไม่ได้เลือกเอนจินสุ่มที่จะใช้เองอย่างชัดเจน ก็จะได้ ตัวสร้างที่ปลอดภัยเชิงเข้ารหัส
ตอนนี้ส่วนที่ยากคือการโน้มน้าวให้ผู้คนย้ายไปใช้ API ใหม่ ยิ่งไปกว่านั้น แม้แต่การย้ายจาก
mt_rand()ที่ใช้ globalMt19937instance ไปเป็นrandom_int()ที่อิง CSPRNG ซึ่งมีให้ใช้มาตั้งแต่ PHP 7.0 แล้ว ก็ยังไม่ง่าย[1] https://www.php.net/releases/8.2/en.php#random_extension
ในกรณีใช้งานของฉัน มีองค์ประกอบอยู่หลายหมื่นชิ้น และเมื่อทำ profiling ก็พบว่าส่วนสำคัญของเวลาในการเริ่มต้นโครงสร้างข้อมูลหมดไปกับ
Read()ของcrypto/randซึ่งบน MacBook ของฉันมันกำลังทำ system callพอแพตช์ไลบรารีให้ใช้
Read()ของmath/randประสิทธิภาพก็ดีขึ้นมากนอกจากที่
math/randเร็วกว่าแล้ว ฉันยังกังวลด้วยว่าจะทำให้ entropy pool ของระบบหมดไปโดยไม่มีเหตุผล ในกรณีนี้ เหตุผลเดียวที่ ID จำเป็นต้องสุ่ม ก็เพื่อให้หลังจาก serialize/deserialize โครงสร้างข้อมูลแล้วจะยังเพิ่มองค์ประกอบต่อภายหลังได้ แต่ฉันไม่ได้ตั้งใจจะทำแบบนั้นฉันไม่แน่ใจว่าช่วงเวลาของการเปลี่ยนแปลงที่บล็อกนี้พูดถึงนั้นตรงกับประสบการณ์ของฉันอย่างไร ถ้าฉันใช้ไลบรารีเวอร์ชันเก่า และตอนนี้
crypto/randแทบจะแยกไม่ออกจากmath/randแล้ว ก็ถือว่าเยี่ยมเลย :-)ขนาด state ยังถือว่าค่อนข้างใหญ่ (64 ไบต์ เทียบกับ 16 ไบต์) แต่ก็ยังดีกว่า
mt19937หรือ PRNG เก่าของ Go มากถ้า CSPRNG ช้ากว่านี้มาก อย่างที่มักเป็นกับ CSPRNG ทั่วไปที่ไม่ใช่ ChaCha แบบลดรอบ ความน่าสนใจในฐานะค่าเริ่มต้นก็จะลดลง
ยังมีอีกปัจจัยเล็ก ๆ ที่ผลักให้คนไปทาง PRNG ทั้งที่ไม่ต้องใช้ seed คือ API ของ CSPRNG มักมี error ที่ต้องจัดการเสมอ เผื่อกรณี system call ล้มเหลวหรือ entropy ไม่พอ
การอ่านจาก
crypto/randจริง ๆ แล้วล้มเหลวบ่อยแค่ไหน? บนระบบสมัยใหม่ต้องอ่านมากแค่ไหนถึงจะทำให้ entropy หมด? ฉันไม่เคยเห็นมันล้มเหลวเลยแม้ในระดับหลายพันล้านคำขอ และddก็ทำงานได้ปกติเลยสงสัยว่าในกรณีใช้งานส่วนใหญ่ API แบบ
Must/panicอาจเหมาะเป็นค่าเริ่มต้นหรือเปล่าอีกอย่าง ฉันไปดูแพ็กเกจ
secretsของ Python (https://docs.python.org/3/library/secrets.html) แล้วไม่เจอการพูดถึงว่ามันอาจ throw exception เลย ในการใช้งานจริงมันเป็นสิ่งที่แทบไม่เกิดขึ้นใช่ไหม?ยอมเสียประสิทธิภาพเล็กน้อย เพื่อแลกกับหลักประกันที่แข็งแรงกว่ามากว่าจะไม่สร้างหายนะจากการใช้ตัวสร้างเลขสุ่มผิดประเภท
น่าเสียดายที่แทบทุกภาษายังปล่อยให้นักพัฒนาต้องคอยระวัง จุดคม ๆ นี้เอง
เผื่อใครยังไม่รู้
gosecและส่วนขยายของมันอย่างgolangci-lintจะเตือนเมื่อมีการใช้math/randhttps://github.com/securego/gosec/blob/d3b2359ae29fe344f4df5...
math/rand/v2คือในที่ทำงานสามารถใช้มันได้โดยไม่ต้องมีnolintdirective และไม่ต้องมีการถกกันใน PR ต่อท้ายฉันยังตีความคำแนะนำเกี่ยวกับความปลอดภัยและตัวเลือก v2 ใหม่อยู่
ในบล็อกโพสต์มีประโยคประมาณว่า “สำหรับค่าลับต้องใช้อย่างอื่น” แล้วก็อธิบายต่ออย่างละเอียดทั้งเรื่องความสุ่มเชิงเข้ารหัส ChaCha8 และการ seed จากเลขสุ่มของระบบ จนให้ความรู้สึกว่า “ปลอดภัย” มาก
แต่ในเอกสารของแพ็กเกจเขียนไว้แบบนี้
... but it should not be used for security-sensitive work ... This package's outputs might be easily predictable regardless of how it's seeded. For random numbers suitable for security-sensitive work, see the crypto/rand package.ถ้าอย่างนั้นทำไมในบล็อกโพสต์ถึงสื่อเหมือนว่า
math/rand/v2ใช้กับ “ค่าลับ” ได้?พูดสั้น ๆ คือ สิ่งที่อ่อนไหวยังคงต้องใช้
crypto/randทั้งหมด และการปรับปรุงที่อธิบายไว้ตรงนี้หมายถึงเป็น ตาข่ายนิรภัย เวลามีคนใช้math/rand/v2ผิดที่ผิดทางใช่ไหม?math/rand/v2อาจไม่ใช่ตัวเลือกที่ดีที่สุด แต่ก็ไม่ใช่ว่าถ้าเผลอใช้มันในจุดที่ควรใช้crypto/randแล้วจะกลายเป็นช่องโหว่ร้ายแรงทันทีอีกต่อไปในบทความก็มีบอกไว้แบบนี้
crypto/randยังคงเป็นตัวเลือกที่ดีกว่า ระบบเคอร์เนลของระบบปฏิบัติการสามารถเก็บค่าตัวเลขสุ่มให้เป็นความลับได้ดีกว่าในการรับมือกับการโจมตีแบบแอบสังเกตหลายรูปแบบ เคอร์เนลยังเติม entropy ใหม่ให้กับตัวสร้างอย่างต่อเนื่อง และผ่านการตรวจสอบมาแล้วมากกว่า แต่การเผลอใช้math/randก็ไม่ใช่หายนะด้านความปลอดภัยอีกต่อไปแล้ว`แม้แต่ใน benchmark ที่แย่ที่สุด กลยุทธ์ใหม่ก็ช้ากว่าเครื่องกำเนิดเลขสุ่มที่ไม่ปลอดภัยเพียงประมาณครึ่งหนึ่งเท่านั้น และ benchmark ส่วนใหญ่ก็ใกล้เคียงกันมากกว่านั้น
Go วาง สมดุลระหว่างความปลอดภัยกับประสิทธิภาพ ได้ดีสำหรับ standard library และแอปที่สร้างอยู่บนมัน หวังว่า ecosystem อื่น ๆ จะทำตามได้บ้าง
ถ้าแอปพลิเคชันต้องการเลขสุ่มที่เร็วแต่ไม่ปลอดภัย ก็ต้องไป implement generator ภายในขึ้นมาเอง
การวางเลขสุ่มที่ไม่ปลอดภัยไว้ในจุดที่หยิบใช้ได้ง่าย เป็น เครื่องมือยิงเท้าตัวเอง ที่ควรเก็บให้พ้นมือ
การผลักดันให้คนคิดว่า primitive
"random"นั้นปลอดภัยเชิงเข้ารหัส เป็นการส่งเสริมแนวปฏิบัติที่ไม่ดีการทำให้
math/rand/v2ปลอดภัยเชิงเข้ารหัสอาจแก้ปัญหาหนึ่งได้ แต่ตอนนี้สิ่งที่ดูเหมือนไม่ได้รับประกันด้านความปลอดภัยก็กลายเป็นสิ่งที่ “พอใช้ได้” ไปแล้วโดยทั่วไปแล้ว ฟังก์ชัน
math/randไม่มีธรรมเนียมว่าต้องปลอดภัยเชิงเข้ารหัส การเปลี่ยนจุดนี้เพื่อให้โค้ดที่ไม่ดีบังเอิญทำงานได้ถูกต้อง อาจไปบดบังว่าเรากำลังทำพลาดอย่างชัดเจนแบบนี้อยู่ แล้วอาจมีความผิดพลาดอื่นอะไรอีกบ้างmath/randของ Go 1 ถ้าจะพูดให้แม่นกว่านี้ ควรเรียกว่า additive lagged Fibonacci generatorการเผยแพร่ครั้งแรกคือบทความของ Green, Smith และ Klem
[1] https://doi.org/10.1145/320998.321006
ผมรู้จัก https://www.leviathansecurity.com/blog/attacking-gos-lagged-... อยู่แล้ว และที่นั่นก็เรียกมันว่า lagged Fibonacci generator เหมือนกัน
เมื่อไม่กี่เดือนก่อน Rob Pike กับผมได้แลกอีเมลกับ Don Mitchell ผู้เขียน C เวอร์ชันดั้งเดิมของ generator ใน Go 1 เพื่อถามว่าเขาอธิบาย algorithm นี้อย่างไร และเขาตอบมาว่า “เท่าที่จำได้ Jim กับผมได้ implement generator คล้าย LFSR ของ Marsaglia”
ผมมองว่าทั้งสองคำอธิบาย คือ lagged Fibonacci และ generator แบบคล้าย LFSR ต่างก็ถูกต้องทั้งคู่ เพียงแต่มองคนละมุม จะใช้แบบไหนก็ได้ แต่ในบทความนี้เราเลือกใช้คำอธิบายของผู้สร้างดั้งเดิม
ถ้าจะติเล็กน้อย จุดนี้ดูเหมือนจะใช้ ความสุ่มเชิงสถิติ กับ เครื่องกำเนิดเลขสุ่มเทียม ปะปนกัน
คำนิยามของความสุ่มเชิงสถิติในวิกิคือ “ลำดับตัวเลขจะถือว่าสุ่มเชิงสถิติเมื่อไม่มีรูปแบบหรือความเป็นระเบียบที่สังเกตได้”
คำนิยามนี้ใช้กับเครื่องกำเนิดเลขสุ่มแท้จริง (TRNG) ได้ไหม? ก็ควรจะใช้ได้ อย่างน้อยในระยะยาวหรือที่ขีดสุดควรเป็นเช่นนั้น ไม่อย่างนั้นมันก็ไม่ใช่ TRNG
TRNG ควรสร้าง “ลำดับตัวเลขที่ไม่มีรูปแบบหรือความเป็นระเบียบที่สังเกตได้” ในระยะยาว
ดังนั้นความสุ่มเชิงสถิติจึงไม่ได้หมายถึง PRNG เสมอไป แต่ก็อาจกล่าวได้ว่าใช้กับ TRNG ได้เช่นกัน
ดูเหมือนปัญหาจะมาจากการที่มีแบบทดสอบความสุ่มเชิงสถิติจำนวนมากสำหรับตรวจสอบว่า PRNG มีความสุ่มเชิงสถิติในรูปแบบที่จำกัดหรือไม่
เพราะฉะนั้น หากต้องการระบุ PRNG ให้ชัด คำว่า “เครื่องกำเนิดเลขสุ่มเทียม” น่าจะเหมาะกว่า “ความสุ่มเชิงสถิติ” แต่ก็เป็นเพียงจุดติเล็กน้อยเท่านั้น