2 คะแนน โดย GN⁺ 2024-05-08 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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 สองค่า
  • 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.Source ChaCha8Rand ไม่ 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 ด้วย JavaScript Math.random เมื่อไม่มี Web Crypto API
  • ความปลอดภัยของระบบไม่อาจตั้งอยู่บนสมมติฐานว่านักพัฒนาจะไม่ทำพลาด
  • ChaCha8Rand ของ Go 1.22 แสดงให้เห็นว่าแนวทางการใช้ตัวสร้างเลขสุ่มเทียมที่แข็งแกร่งเชิงวิทยาการเข้ารหัสลับแม้กับเลขสุ่ม “เชิงคณิตศาสตร์” ก็สามารถให้ประสิทธิภาพที่แข่งขันกับตัวสร้างอื่นได้

1 ความคิดเห็น

 
GN⁺ 2024-05-08
ความคิดเห็นจาก Hacker News
  • อย่างที่บทความบอกไว้ rclone เคยพลาดแบบนี้เป๊ะ
    ระหว่างรีแฟกเตอร์โค้ดที่ใช้ Read ของ crypto/rand การ import ถูกเปลี่ยนอัตโนมัติ และน่าจะปนกับโค้ดที่ใช้ math/rand จน goimports เปลี่ยนเป็น math/rand
    ผลก็คือแทนที่จะใช้ ตัวสร้างเลขสุ่มเชิงความปลอดภัย กลับกลายเป็นว่า rclone ใช้ ตัวสร้างแบบกำหนดผลลัพธ์ได้ ที่ seed ด้วยเวลา และไม่มีใครสังเกตจาก diff :-(
    https://www.cvedetails.com/cve/CVE-2020-28924/
    เพราะงั้นผมเห็นด้วยมากกับการเปลี่ยนครั้งนี้

    • ฟังแล้วเจ็บเลย ขอโทษด้วย ในปี 2016 มีการเปลี่ยน 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" ได้
      เป็นไปได้ว่าอาจโดนในช่วงนั้นพอดี
    • ดูไม่น่าจะยากอะไรนัก ถ้าจะมี API ชื่อประมาณ "PredictableRand"
  • เมื่อสัปดาห์ก่อน spacey ก็โพสต์เรื่องนี้ไว้ที่ https://news.ycombinator.com/item?id=40237491 แต่โพสต์นั้นน่าจะถูกกลบผิด ๆ ว่าเป็นโพสต์ซ้ำของ https://news.ycombinator.com/item?id=40224864
    บทความในบล็อก go.dev สองชิ้นนี้เป็นคนละตอนในซีรีส์เดียวกัน แต่ต่างกันพอสมควร โดยบทความนี้ว่าด้วย อัลกอริทึมการสร้างเลขสุ่มเชิงความปลอดภัยที่มีประสิทธิภาพ ส่วนบทความก่อนหน้านี้พูดถึง การออกแบบ API ของ Go

  • Russell Cox ปล่อยทั้งบล็อกเทคนิค ข้อเสนอ และผลงานดี ๆ ออกมาอย่างต่อเนื่อง
    ถ้าอยากพัฒนาความชัดเจนในการเขียนและการคิด การเริ่มจาก Russell Cox ถือเป็นจุดเริ่มต้นที่ดี

    • ซีรีส์ของเขาเรื่อง ออโตมาตาสถานะจำกัดและ regex ทำให้ผมอินกับด้านนี้มาก
      ตอนนั้นผมยังไม่รู้ด้วยซ้ำว่า Russ Cox คือใคร แต่ซีรีส์นั้นยอดเยี่ยมจริง ๆ
      ถ้าพูดถึงแหล่งความรู้ฟรีเรื่องการอิมพลีเมนต์ regex นี่น่าจะคุณภาพดีที่สุดแล้ว รองลงไปก็คือหนังสือแนวคอมไพเลอร์หลายเล่ม แต่พวกนั้นไม่ฟรีและก็หาอ่านบนเว็บได้ไม่ง่าย
    • เขายังทำเดโมวิดีโอได้ดีด้วย https://research.swtch.com/acme
  • ผมเองก็เคยใช้ math/rand ในจุดที่ควรต้องใช้ crypto/rand
    ผลคือ dnscrypt-proxy2 เวอร์ชันแรก ๆ ใช้ คีย์แบบคงที่
    สาเหตุมาจากส่วนขยาย VSCode ที่เพิ่ม import ให้อัตโนมัติ ผมระวัง import crypto/rand ด้วยตัวเองในทุกไฟล์ที่ต้องใช้เลขสุ่มเชิงความปลอดภัย แต่พลาดไปไฟล์หนึ่ง ทุกอย่างคอมไพล์ได้และทำงานปกติ แล้วผมก็ไม่ทันสังเกตว่าส่วนขยายนั้นแอบเพิ่ม import math/rand ให้ในไฟล์นั้น
    หลังจากนั้นผมก็ import crypto/rand โดยตั้งชื่อแฝงเป็น cryptorand เพื่อหลีกเลี่ยงการ auto import rand ผิดตัว
    อีกอย่าง Zig ก็ใช้ ตัวสร้างเลขสุ่มที่อิง ChaCha8 และในการทำงานเข้ารหัส ผู้ใช้ไม่สามารถส่งตัวสร้างของตัวเองเข้าไปได้ โดยระบบจะใช้ตัวที่ปลอดภัยเสมอ ส่วนการทดสอบ ฟังก์ชันบางตัวรับ seed แบบชัดเจนได้
    สำหรับสภาพแวดล้อมที่มีข้อจำกัด ใน standard library ยังมีตัวสร้างที่เล็กกว่าซึ่งอิง Ascon permutation และ Reverie construction รวมอยู่ด้วย

    • ไม่แน่ใจว่าในกรณีของคุณเกิดอะไรขึ้นกันแน่ แต่มีโอกาสสูงว่าจะไม่ใช่อย่างที่อธิบายมา
      ในปี 2016 มีการเปลี่ยน goimports ให้เลือก crypto/rand แทน math/rand แล้ว (https://go-review.googlesource.com/24847) และตอนนั้นก็ยังไม่มีการรองรับ Go บน VSCode ด้วย
    • มีคนอื่นพูดแบบเดียวกันเหมือนกัน พูดตรง ๆ เลยว่าธรรมเนียมการเพิ่ม import อัตโนมัติดูประหลาดสุด ๆ และทำลายจุดประสงค์ของการแยกชื่อออกเป็นคนละ namespace
  • แม้จะเข้าสู่ยุค 2020 แล้ว ฉันก็ยังคิดอยู่บ่อย ๆ ว่าทำไมการติดตั้งตัวสร้างเลขสุ่มเริ่มต้นของหลายภาษาโปรแกรมยังใช้ตัวสร้างเลขสุ่มที่เร็วอย่าง LFSR หรือ MT
    ดูเหมือนว่าการตั้งแบบเผื่อความปลอดภัย โดยสมมติว่าผู้คนไม่รู้ว่าตัวเองต้องการ ตัวสร้างเลขสุ่มเทียม หรือ ตัวสร้างเลขสุ่มเทียมที่ปลอดภัยเชิงเข้ารหัส แล้วเปลี่ยนค่าเริ่มต้นไปเป็นอย่างหลัง และให้เฉพาะคนที่ต้องการอย่างแรกเป็นผู้เลือกอย่างชัดเจน จะดีกว่า

    • API สุ่มแบบ object-oriented ใหม่ของ PHP 8.2 ก็ทำแบบนั้นอย่างชัดเจน
      ถ้านักพัฒนาไม่ได้เลือกเอนจินสุ่มที่จะใช้เองอย่างชัดเจน ก็จะได้ ตัวสร้างที่ปลอดภัยเชิงเข้ารหัส
      ตอนนี้ส่วนที่ยากคือการโน้มน้าวให้ผู้คนย้ายไปใช้ API ใหม่ ยิ่งไปกว่านั้น แม้แต่การย้ายจาก mt_rand() ที่ใช้ global Mt19937 instance ไปเป็น random_int() ที่อิง CSPRNG ซึ่งมีให้ใช้มาตั้งแต่ PHP 7.0 แล้ว ก็ยังไม่ง่าย
      [1] https://www.php.net/releases/8.2/en.php#random_extension
    • เมื่อไม่นานมานี้ ฉันเริ่มใช้ไลบรารี Go ใหม่ที่สร้าง ID แบบสุ่มให้กับองค์ประกอบหลายส่วนในโครงสร้างข้อมูลที่ซับซ้อน
      ในกรณีใช้งานของฉัน มีองค์ประกอบอยู่หลายหมื่นชิ้น และเมื่อทำ profiling ก็พบว่าส่วนสำคัญของเวลาในการเริ่มต้นโครงสร้างข้อมูลหมดไปกับ Read() ของ crypto/rand ซึ่งบน MacBook ของฉันมันกำลังทำ system call
      พอแพตช์ไลบรารีให้ใช้ Read() ของ math/rand ประสิทธิภาพก็ดีขึ้นมาก
      นอกจากที่ math/rand เร็วกว่าแล้ว ฉันยังกังวลด้วยว่าจะทำให้ entropy pool ของระบบหมดไปโดยไม่มีเหตุผล ในกรณีนี้ เหตุผลเดียวที่ ID จำเป็นต้องสุ่ม ก็เพื่อให้หลังจาก serialize/deserialize โครงสร้างข้อมูลแล้วจะยังเพิ่มองค์ประกอบต่อภายหลังได้ แต่ฉันไม่ได้ตั้งใจจะทำแบบนั้น
      ฉันไม่แน่ใจว่าช่วงเวลาของการเปลี่ยนแปลงที่บล็อกนี้พูดถึงนั้นตรงกับประสบการณ์ของฉันอย่างไร ถ้าฉันใช้ไลบรารีเวอร์ชันเก่า และตอนนี้ crypto/rand แทบจะแยกไม่ออกจาก math/rand แล้ว ก็ถือว่าเยี่ยมเลย :-)
    • หนึ่งในเหตุผลที่ดีกว่าสำหรับการใช้ CSPRNG ซึ่งในที่นี้คือ ChaCha8 คือในการ benchmark มันช้ากว่า PCG ไม่ถึง 2 เท่า
      ขนาด state ยังถือว่าค่อนข้างใหญ่ (64 ไบต์ เทียบกับ 16 ไบต์) แต่ก็ยังดีกว่า mt19937 หรือ PRNG เก่าของ Go มาก
      ถ้า CSPRNG ช้ากว่านี้มาก อย่างที่มักเป็นกับ CSPRNG ทั่วไปที่ไม่ใช่ ChaCha แบบลดรอบ ความน่าสนใจในฐานะค่าเริ่มต้นก็จะลดลง
    • มีกรณีอื่นอะไรอีกไหมที่ต้องการแบบแรก? ที่ฉันนึกออกมีแค่การใช้ seed คงที่เพื่อให้ได้ผลลัพธ์ที่ทำซ้ำได้ เช่น ในการทดสอบหรือการตรวจสอบความถูกต้อง
      ยังมีอีกปัจจัยเล็ก ๆ ที่ผลักให้คนไปทาง 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 เลย ในการใช้งานจริงมันเป็นสิ่งที่แทบไม่เกิดขึ้นใช่ไหม?
    • ฉันชอบแนวคิดที่ว่า “เลขสุ่มทั้งหมดของระบบควรมาจาก CSPRNG เว้นแต่จะเลือกออกจากแนวทางนี้อย่างชัดเจน”
      ยอมเสียประสิทธิภาพเล็กน้อย เพื่อแลกกับหลักประกันที่แข็งแรงกว่ามากว่าจะไม่สร้างหายนะจากการใช้ตัวสร้างเลขสุ่มผิดประเภท
      น่าเสียดายที่แทบทุกภาษายังปล่อยให้นักพัฒนาต้องคอยระวัง จุดคม ๆ นี้เอง
  • เผื่อใครยังไม่รู้ gosec และส่วนขยายของมันอย่าง golangci-lint จะเตือนเมื่อมีการใช้ math/rand
    https://github.com/securego/gosec/blob/d3b2359ae29fe344f4df5...

    • สิ่งหนึ่งที่ฉันชอบที่สุดใน math/rand/v2 คือในที่ทำงานสามารถใช้มันได้โดยไม่ต้องมี nolint directive และไม่ต้องมีการถกกันใน 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

    • ดูเหมือนว่าในบทความนั้นจะไม่ได้พูดถึงส่วนที่เป็น “lagged” หรือไม่ก็อาจเป็นผมที่มองข้ามไป
      ผมรู้จัก 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 ให้ชัด คำว่า “เครื่องกำเนิดเลขสุ่มเทียม” น่าจะเหมาะกว่า “ความสุ่มเชิงสถิติ” แต่ก็เป็นเพียงจุดติเล็กน้อยเท่านั้น