- ใน Go 1.22 มีการเปลี่ยนให้ทั้งแพ็กเกจ
math/rand เดิมและแพ็กเกจ math/rand/v2 ที่เพิ่งเพิ่มเข้ามา ใช้ตัวสร้างเลขสุ่มที่ปลอดภัยเชิงเข้ารหัส ทำให้ได้ความสุ่มที่ดีกว่าเดิม และลดความเสียหายที่อาจเกิดขึ้นอย่างมากเมื่อผู้พัฒนาเผลอใช้ math/rand แทน crypto/rand
ความต่างระหว่างความสุ่มเชิงสถิติกับความสุ่มเชิงเข้ารหัส
- ความสุ่มเชิงสถิติเหมาะกับงานอย่างการจำลอง การสุ่มตัวอย่าง การวิเคราะห์เชิงตัวเลข อัลกอริทึมสุ่มที่ไม่เกี่ยวกับการเข้ารหัส การทดสอบแบบสุ่ม การสลับลำดับข้อมูลอินพุต และ random exponential backoff
- แม้จะเป็นสูตรคณิตศาสตร์พื้นฐานที่คำนวณได้ง่าย ก็เพียงพอสำหรับงานลักษณะนี้แล้ว แต่ผู้สังเกตการณ์ที่รู้ว่าใช้อัลกอริทึมอะไรอยู่ เมื่อเห็นค่าจำนวนหนึ่งก็อาจคาดเดาลำดับถัดไปได้
- ความสุ่มเชิงเข้ารหัสนั้น ต้องไม่สามารถคาดเดาได้จริง แม้จะสังเกตค่าที่สร้างออกมาก่อนหน้านั้นไปแล้วก็ตาม
- โปรโตคอลการเข้ารหัสที่ปลอดภัย กุญแจลับ การพาณิชย์สมัยใหม่ และความเป็นส่วนตัวออนไลน์ ต่างพึ่งพาความสุ่มเชิงเข้ารหัสอย่างมาก
ตัวสร้าง math/rand ใน Go 1
- ใช้วิธี Linear-feedback shift register (LFSR)
- มีปัญหาที่สถานะภายในถูกเปิดเผยได้ทั้งหมด เพราะประกอบด้วยเวกเตอร์ของ
uint64 จำนวน 607 ค่า
- หากอ่านค่าจากตัวสร้าง 607 ค่า ก็จะเปิดเผยสถานะทั้งหมดและคาดเดาค่าถัดไปได้
ตัวสร้าง PCG ของ math/rand/v2
- ใช้อัลกอริทึม PCG ของ Melissa O'Neill เป็น LCG แบบ 128 บิตที่มีการ post-processing
- สถานะทั้งหมดคือเลข 128 บิตเพียงตัวเดียว โดยอัปเดตด้วยการคูณและบวกแบบ 128 บิต
- ใน Go ใช้ฟังก์ชัน scramble แบบอิงการคูณแทนแบบอิง XOR ตามข้อเสนอของ O'Neill เพื่อผสมบิตให้เข้มข้นขึ้น
- แม้จะคำนวณมากกว่าตัวสร้างของ Go 1 แต่ใช้หน่วยความจำเก็บสถานะน้อยกว่ามาก ไวต่อค่าเริ่มต้นน้อยกว่า และยังผ่านการทดสอบเชิงสถิติที่ตัวสร้างอื่นไม่ผ่าน
- แต่ PCG ก็ยังไม่ใช่สิ่งที่คาดเดาไม่ได้อย่างแท้จริง
ความสุ่มเชิงเข้ารหัส
- ในที่สุดแล้ว ระบบปฏิบัติการต้องรวบรวมความสุ่มจริงจากสัญญาณรบกวนของอุปกรณ์ทางกายภาพ
- เมื่อรวบรวมความสุ่มได้เพียงพอแล้ว (256 บิตขึ้นไป) ก็สามารถขยายด้วยแฮชเชิงเข้ารหัสหรืออัลกอริทึมเข้ารหัส เพื่อสร้างลำดับเลขสุ่มที่ยาวได้ตามต้องการ
- แพ็กเกจ
crypto/rand ของ Go ทำหน้าที่ abstract ความแตกต่างของอินเทอร์เฟซระบบปฏิบัติการเหล่านี้ และให้ใช้อินเทอร์เฟซเดียวกันคือ rand.Read
ตัวสร้าง ChaCha8Rand
- เป็นตัวสร้างใหม่ที่ดัดแปลงมาจากสตรีมไซเฟอร์ ChaCha ของ DJB
- ใช้ ChaCha8 ซึ่งเป็นเวอร์ชัน 8 รอบ ปลอดภัยและเร็วกว่า ChaCha20 ถึง 2.5 เท่า
- ใช้ seed ขนาด 32 ไบต์เป็นกุญแจของ ChaCha8 และทุก ๆ 16 บล็อก จะนำ 32 ไบต์ท้ายของบล็อกที่สร้างขึ้นมาเป็นกุญแจของ 16 บล็อกถัดไป เพื่อให้มี forward secrecy
rand.Float64, rand.N เป็นต้น ใน math/rand/v2 จะใช้ตัวสร้างนี้เสมอ
math/rand ก็ใช้ตัวสร้างนี้เช่นกัน ยกเว้นเมื่อมีการเรียก rand.Seed จึงจะกลับไปใช้ตัวสร้างของ Go 1
- ตัวรันไทม์ก็ใช้ ChaCha8Rand ในการเลือก hash seed ของ map ใหม่ด้วย
การแก้ปัญหาความผิดพลาดด้านความปลอดภัย
- Go 1.22 ทำให้
math/rand แข็งแกร่งขึ้น ส่งผลให้โปรแกรมปลอดภัยขึ้นได้โดยไม่ต้องแก้โค้ด
- ตัวอย่างเช่น หากใช้
math/rand Read ผิดวัตถุประสงค์เพื่อนำไปสร้างกุญแจ ใน Go 1.20 นี่คือปัญหาความปลอดภัยร้ายแรง แต่ใน Go 1.22 จะกลายเป็นเพียงความผิดพลาดธรรมดา
- แม้แต่งานที่ดูไม่เกี่ยวกับ “การเข้ารหัส” อย่างการสร้าง UUID หรือการกระจายโหลดของเซิร์ฟเวอร์ฝั่งฟรอนต์เอนด์ การใช้ ChaCha8Rand ก็ทำให้แข็งแกร่งกว่าตัวสร้างของ Go 1 มาก
ประสิทธิภาพ
- ChaCha8Rand มีประสิทธิภาพใกล้เคียงกับตัวสร้างของ Go 1 และ PCG
- บนโค้ดแบบ 32 บิต ChaCha8Rand เร็วกว่า PCG ที่ต้องใช้การคูณ 128 บิต
- ด้วยอัลกอริทึมของ
math/rand/v2 ที่หลีกเลี่ยงการหาร 64 บิต การทำงาน N(1000) จึงมีบางกรณีที่ ChaCha8Rand หรือ PCG เร็วกว่าตัวสร้างของ Go 1
- โดยรวมแล้ว ChaCha8Rand ช้ากว่าตัวสร้างของ Go 1 แต่ไม่มีกรณีที่ช้ากว่าเกิน 2 เท่า และบนเซิร์ฟเวอร์ทั่วไปความต่างไม่เกิน 3ns
ความเห็นของ GN⁺
- การนำ ChaCha8Rand มาใช้ใน Go 1.22 ถือเป็นตัวอย่างการปรับปรุงระดับภาษาที่ดีเยี่ยม ซึ่งยกระดับความปลอดภัยได้มากโดยแทบไม่กระทบประสิทธิภาพ น่าประทับใจที่สามารถปิดช่องพลาดที่ผู้พัฒนามักทำกันบ่อยได้ตั้งแต่ระดับภาษา
- อย่างที่กล่าวในบทความ ความผิดพลาดลักษณะนี้ไม่ได้เกิดเฉพาะกับ Go แต่พบได้บ่อยในภาษาอื่นเช่นกัน ความปลอดภัยของระบบไม่ควรขึ้นอยู่กับความผิดพลาดของผู้พัฒนาเพียงอย่างเดียว ดังนั้นภาษาอื่นก็ควรเดินหน้าในทิศทางเดียวกับ Go คือใช้ตัวสร้างเลขสุ่มเทียมที่แข็งแกร่งเชิงเข้ารหัส แม้ในงานที่ดูเป็นเลขสุ่มแบบ “คณิตศาสตร์”
- อย่างไรก็ดี ChaCha8Rand ไม่เหมาะสำหรับใช้ใน primitive การเข้ารหัสอย่าง
crypto_box หรือ xchacha20poly1305 หากเป็นงานลักษณะนี้ก็ยังควรใช้ crypto/rand โดยตรง
- การที่ Go runtime เปลี่ยนมาใช้ ChaCha8Rand แม้กระทั่งกับการเลือก hash seed ของ map ก็ดูน่าประหลาดใจอยู่บ้าง แม้จะยังไม่ชัดว่าจำเป็นต้องใช้เลขสุ่มเชิงเข้ารหัสกับ hash seed หรือไม่ แต่ก็สะท้อนให้เห็นถึงแนวคิดด้านความปลอดภัยของทีมพัฒนาที่ต้องการปิดโอกาสการโจมตีที่น่าปวดหัวตั้งแต่ต้นทาง
- เมื่อคุณภาพของ
math/rand ซึ่งเป็นแพ็กเกจมาตรฐานระดับภาษาดีขึ้นแล้ว ต่อไปก็น่าจะมีการใช้งาน math/rand โดยตรงในแอปพลิเคชันมากขึ้น หากมีโปรเจกต์ที่ก่อนหน้านี้ต้องใช้ไลบรารีเลขสุ่มแยกต่างหากเพราะกังวลเรื่องความสามารถในการคาดเดาของ math/rand การเปลี่ยนแปลงครั้งนี้ก็น่าจะให้ประโยชน์อย่างชัดเจน
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
สรุปได้ดังนี้:
Readของแพ็กเกจmath/randถูก deprecated และพบกรณีที่มีการนำไปใช้ผิดแทนcrypto/randซึ่งนำไปสู่ความผิดพลาดในการใช้ตัวสร้างเลขสุ่มเชิงกำหนดที่ไม่ปลอดภัยด้านความมั่นคงปลอดภัยgosecหรือgolangci-lintจะแจ้งเตือนเมื่อมีการใช้math/randmath/rand/v2ใช้ไซเฟอร์ ChaCha8 และ seed จาก system entropy ทำให้ดูเหมือนว่า "ปลอดภัย" แต่ก็ยังไม่เหมาะสำหรับงานที่อ่อนไหวด้านความปลอดภัย ซึ่งควรใช้crypto/randmath/randของ Go 1 หากอธิบายให้แม่นยำคือ additive lagged Fibonacci generatormath/randตัวใหม่ แม้ในกรณีแย่ที่สุดก็ยังมีความเร็วราวครึ่งหนึ่งของตัวสร้างเลขสุ่มแบบไม่ปลอดภัยเดิม และในเบนช์มาร์กส่วนใหญ่แทบไม่ต่างกันเลย Go กำลังสร้างสมดุลที่เหมาะสมระหว่างความปลอดภัยและประสิทธิภาพใน standard libraryjava.util.Randomของ Java