ความยากของการสร้างจำนวนเฉพาะ 1024 บิต
(glitchcomet.com)- เป็นการทดลองที่ สร้างจำนวนเฉพาะขนาดประมาณ 1024 บิตสองจำนวนด้วย Rust โดยตรง สำหรับใช้กับคีย์ RSA 2048 บิต โดยลงมือทำเองตั้งแต่การสร้างเลขสุ่มไปจนถึงการคำนวณจำนวนเต็มขนาดใหญ่โดยไม่พึ่งพาไลบรารีภายนอก
- trial division แบบง่ายเพียงอย่างเดียวเพียงพอสำหรับ 16 บิตที่ใช้เวลาราว 40ms แต่แม้จะปรับแต่งแล้วก็ยังใช้เวลา 6.4 วินาทีใน 64 บิต ทำให้ขยายไปถึง 1024 บิตได้ยาก
- การทดสอบแบบ Fermat เร็วก็จริง แต่ไม่สามารถกรอง pseudoprime ได้ทั้งหมด จึงใช้การทดสอบ Miller-Rabin ที่ k=10 สำหรับการตัดสินขั้นสุดท้าย
- เพื่อก้าวข้ามข้อจำกัดของชนิดจำนวนเต็มพื้นฐาน จึงสร้าง BigInt ขึ้นมาเอง และเปลี่ยนโครงสร้างจากอาร์เรย์
boolเป็นอาร์เรย์ไบต์ แล้วต่อด้วยชังก์u64ทำให้เวลาสร้างจำนวนเฉพาะ 1024 บิตลดจากราว 32 นาทีเหลือระดับ 60~90 วินาที - เวอร์ชันสุดท้ายรวม BigInt แบบชังก์
u64, การหารแบบเร็ว, trial division ด้วยจำนวนเฉพาะขนาดเล็ก, การเพิ่มค่าผู้สมัครทีละ+2, และการประมวลผลขนาน 16 เธรด จนหาจำนวนเฉพาะ 1024 บิตได้เฉลี่ยราว 40ms แต่ยังไม่ใช่ไลบรารีเข้ารหัสที่ผ่านการตรวจสอบ
สร้างจำนวนเฉพาะ 1024 บิตสำหรับ RSA ด้วยตัวเอง
- เป้าหมายคือการสร้าง จำนวนเฉพาะที่สามารถใช้สร้างคีย์ RSA ได้ ด้วยตัวเอง
- คีย์ RSA 2048 บิตสร้างจากผลคูณของจำนวนเฉพาะสองจำนวน ดังนั้นแต่ละจำนวนจึงต้องมีขนาดประมาณ 1024 บิต
- โจทย์ท้าทายจึงแคบลงมาเป็นการ สร้างจำนวนเฉพาะ 1024 บิต
- การทดลองตั้งเงื่อนไขไว้ 3 ข้อ
- เขียนโค้ดตั้งแต่ต้นและไม่ใช้ไลบรารีภายนอก
- ใช้โน้ตบุ๊กที่มี CPU AMD Ryzen 7 และ RAM 16GB โดยไม่พึ่งฮาร์ดแวร์ภายนอกหรือคลาวด์
- ต้องสร้างจำนวนเฉพาะให้ได้ภายใน “เวลาที่สมเหตุสมผล”
- ภาษาโปรแกรมที่เลือกคือ Rust ซึ่งกำลังเรียนรู้อยู่ในช่วงนั้น
- มองว่าใกล้กับแนวคิดระดับล่างพอที่จะจัดการเรื่องแบบนี้ได้ และก็ยังอยู่ในระดับสูงพอที่จะทำความเข้าใจโค้ดเป็นส่วน ๆ ได้ง่าย
ข้อจำกัดของ trial division ที่เห็นชัดใน 16 บิตและ 64 บิต
- กระบวนการพื้นฐานคือสร้างเลขสุ่มขนาด N บิตซ้ำ ๆ และจบเมื่อผ่านการทดสอบความเป็นจำนวนเฉพาะ
- การสร้างเลขสุ่มทำโดยอ่าน
/dev/urandomของ Linux โดยตรงแทนการใช้randcrate ของ Rust/dev/urandomเป็นไฟล์อุปกรณ์เสมือนสำหรับเข้าถึง CSPRNG ของเคอร์เนล Linux- เคอร์เนลจะรวบรวมเอนโทรปีจากสภาพแวดล้อมผู้ใช้ แล้ว seed สตรีมไซเฟอร์เชิงกำหนดที่อิง ChaCha20 เป็นระยะ
- สำหรับเลขสุ่ม 16 บิต จะตั้งบิตแรกและบิตสุดท้ายเป็น
1- บิตสุดท้ายเป็น
1เพื่อให้แน่ใจว่าเป็นเลขคี่ - บิตแรกเป็น
1เพื่อให้ใช้ช่วงบิตตามขนาดที่ต้องการครบถ้วน
- บิตสุดท้ายเป็น
- ใน 16 บิต การใช้เพียง trial division โดยหารตรวจจาก
3ถึงsqrt(num)ก็หาจำนวนเฉพาะได้ในเวลาประมาณ 40ms- ตัวอย่างการรันได้ผลลัพธ์
Prime found: 44809และใช้เวลาทั้งหมดประมาณ 0.038 วินาที
- ตัวอย่างการรันได้ผลลัพธ์
- แต่เมื่อขยายไปเป็น 64 บิต trial division แบบตรงไปตรงมาใช้เวลาประมาณ 30 วินาที
- หลังจากนั้นจึงปรับปรุงโดยตรวจเฉพาะค่าผู้สมัครในรูป
6k±1และลองหารด้วยรายการจำนวนเฉพาะขนาดเล็กก่อน - หลังปรับปรุงแล้ว เวลาสร้างจำนวนเฉพาะ 64 บิตอยู่ที่ราว 6.414 วินาที
- หลังจากนั้นจึงปรับปรุงโดยตรวจเฉพาะค่าผู้สมัครในรูป
- เมื่อแม้แต่ 64 บิตยังใช้เวลาระดับ 6 วินาที ก็เห็นชัดว่าวิธีนี้ไปไม่ถึง การสร้างจำนวนเฉพาะ 1024 บิต
เปลี่ยนไปใช้การทดสอบจำนวนเฉพาะแบบความน่าจะเป็น
- มีการสำรวจอัลกอริทึมเชิงกำหนดอย่าง APR-CL และ ECPP แต่พบว่าซับซ้อนทางคณิตศาสตร์มาก และหาเอกสารอธิบายที่เข้าถึงได้ยาก จึงไม่เหมาะจะลงมือเขียนในครั้งนี้
- หลังดูซอร์สโค้ดของ OpenSSL และคำแนะนำของ NIST ก็ยืนยันได้ว่าในงานใช้งานจริงรวมถึง RSA นิยมใช้ การทดสอบจำนวนเฉพาะแบบความน่าจะเป็น กันอย่างแพร่หลาย
- จากนั้นแนวทางจึงเปลี่ยนจากการ “พิสูจน์ว่าเป็นจำนวนเฉพาะ” ไปเป็นการตัดสินว่าเป็น probable prime ด้วยระดับความแม่นยำที่กำหนด
-
การทดสอบแบบ Fermat
- ทฤษฎีบทเล็กของแฟร์มาต์ใช้ความสัมพันธ์ที่ว่า ถ้า
pเป็นจำนวนเฉพาะและaไม่หารด้วยpลงตัว จะได้ว่าa^(p-1) = 1 mod p - การยกกำลังแบบธรรมดาทำให้เกิด overflow ใน
u128จึงต้องเขียน modular exponentiation ขึ้นมา pow()รับเลขชี้กำลังเป็นu32และหากยกu128ด้วยเลขชี้กำลังที่ใหญ่กว่านั้นก็อาจ overflow ได้- แม้แต่การคูณเองก็อาจเกินขอบเขต
u128จึงใช้วิธีชั่วคราวด้วยการเก็บตัวเลข 64 บิตไว้ในu128 - การทดสอบแบบ Fermat ทำงานเร็ว แต่เพราะมี Fermat pseudoprime จึงอาจตัดสินจำนวนประกอบผิดว่าเป็นจำนวนเฉพาะได้
- จำนวนประกอบลักษณะนี้แม้ไม่พบบ่อยแต่ก็มีมากพอ จึงไม่เหมาะจะเชื่อถือ Fermat เพียงอย่างเดียว
- ทฤษฎีบทเล็กของแฟร์มาต์ใช้ความสัมพันธ์ที่ว่า ถ้า
-
การทดสอบ Miller-Rabin
- Miller-Rabin อิงหลักการเดียวกับ Fermat แต่เป็นอัลกอริทึมทดสอบจำนวนเฉพาะแบบความน่าจะเป็นที่แข็งแรงกว่า
- การติดตั้งใช้งานคือแยก
n-1 = 2^s × dออกเป็นกำลังของ 2 แล้วตรวจหลายเงื่อนไข a^d = 1 mod n- หรือมีบาง
0 <= r < sที่ทำให้a^(2^r × d) = n - 1 mod n - ในการทดลอง 128 บิต สามารถหาจำนวนเฉพาะได้ในเวลาประมาณ 0.042 วินาที ซึ่งใกล้เคียงกับ Fermat
- ขอบเขตความผิดพลาดกรณีเลวร้ายที่สุดของ Miller-Rabin คือ
4^-kและสำหรับnขนาดใหญ่โดยเฉลี่ยอยู่ราว8^-k - เมื่อ
k=10ความน่าจะเป็นผิดพลาดเฉลี่ยคำนวณได้เป็น0.000000000931323% - มีการเปรียบเทียบว่าพอ ๆ กับความน่าจะเป็นที่โยนเหรียญ 30 ครั้งติดแล้วออกหัวทั้งหมด หรือ
2^-30 - อย่างไรก็ตาม ในงานเข้ารหัสจริงยังต้องระวังเรื่องการเลือก base แบบสุ่มและเงื่อนไขเชิงปฏิปักษ์มากกว่านี้
สร้าง BigInt ขึ้นมาเอง
- ชนิดจำนวนเต็มพื้นฐานของ Rust ไม่พอสำหรับการจัดการตัวเลขที่ใหญ่เกิน 64 บิต จึงจำเป็นต้องมี จำนวนเต็มความแม่นยำตามต้องการ (BigInt)
- เนื่องจากตั้งเงื่อนไขว่าจะไม่ใช้ bigint crate ภายนอก จึงต้องเขียน BigInt เองด้วย
-
ความพยายามที่ 1: อาร์เรย์หลักของตัวเลข
- ตอนแรกลองเก็บจำนวนขนาดใหญ่เป็นอาร์เรย์ของหลักเลขฐาน 10
- การบวกและคูณทำได้คล้ายการคำนวณด้วยมือ แต่ไปต่อไม่ไหวตรงการหาร จึงเลิกแนวทางนี้
-
ความพยายามที่ 2: อาร์เรย์ไบนารีแบบ
bool- วิธีที่สองคือเก็บตัวเลขเป็นอาร์เรย์ของ 0 และ 1
BigIntใช้อาร์เรย์[bool; 2048]- เพราะการคูณเลข 1024 บิตสองตัวอาจต้องใช้พื้นที่สูงสุด 2048 บิต จึงเตรียมไว้ 2048 บิต
- การบวกและลบเขียนด้วยแนวทาง full adder
- การคูณใช้วิธี shift-and-add ตามคุณสมบัติของเลขฐานสอง
- การหารใช้ binary long division
- ด้วยการติดตั้งใช้งานนี้ สามารถหาจำนวนเฉพาะ 1024 บิตตัวแรกได้สำเร็จ แต่ใช้เวลาประมาณ 32 นาที 44.90 วินาที
- ในเชิงเทคนิคถือว่าบรรลุเป้าหมายแล้ว แต่ยังไม่ตรงเงื่อนไขเรื่อง “เวลาที่สมเหตุสมผล”
-
ความพยายามที่ 3: ชังก์แบบไบต์
- มีการสังเกตว่า
boolแต่ละตัวในอาร์เรย์boolไม่ได้กิน 1 บิต แต่กิน 1 ไบต์ - นั่นหมายความว่า
[bool; 2048]ใช้ 2048 ไบต์ ไม่ใช่ 2048 บิต - จากนั้นจึงเปลี่ยนไปเก็บ 2048 บิตในอาร์เรย์ขนาด 256 ไบต์
- การบวก ลบ คูณ ยังทำงานได้แทบไม่ต้องเปลี่ยนมาก ส่วนการหารก็ปรับให้มองชังก์ไบต์เหมือนรายการบิต
- วิธีนี้ลดเวลาสร้างจำนวนเฉพาะ 1024 บิตลงเหลือ 4 นาที 43 วินาที
- มีการสังเกตว่า
-
ความพยายามที่ 4: ชังก์
u64- วิธีชังก์ไบต์จริง ๆ แล้วคือ BigInt แบบใช้ “หลักเลข” ที่มีฐานสูงขึ้น
- ขั้นถัดไปคือเก็บ 2048 บิตเป็นชังก์
u64จำนวน 32 ตัว - แต่ละชังก์ทำหน้าที่เหมือน “หลัก” หนึ่งหลัก
- ใช้
u128เพื่อเก็บผลลัพธ์จากการคูณชังก์u64สองตัว - ในโครงสร้างนี้ ตัวเลข 1024 บิตสามารถแทนได้ด้วยชังก์
u6416 ตัว แทนที่จะเป็นเลขฐาน 10 ยาว 309 หลัก - เวลาสร้างจำนวนเฉพาะ 1024 บิตดีขึ้นเป็น 60~90 วินาที
การปรับแต่งคอขวด
- ใน benchmark แบบง่าย ๆ ความแตกต่างระหว่างการติดตั้งใช้งานแบบไบนารีกับแบบชังก์
u64เห็นได้ชัดมากa + bและa - b: 5537.35ns → 123.57nsa * b: 1292283.14ns → 842.32nsa / bและa % b: 733446.76ns → 44440.12nsa < bและa > b: 2506.02ns → 58.91ns
- หลังจากนั้นการปรับแต่งจึงโฟกัสหลัก ๆ ที่การหาร การคูณ การคำนวณภายใน Miller-Rabin และตรรกะการสร้างค่าผู้สมัคร
-
การหาร
- คอขวดที่ใหญ่ที่สุดคือ การหาร
- แม้ใช้โครงสร้างแบบชังก์
u64แล้ว การหารเดิมก็ยังทำ long division ทีละบิตอยู่ดี - จึงอ้างอิงอัลกอริทึมในหน้า 598 ของ Handbook of Applied Cryptography เพื่อเขียน long division แบบอิง radix
- วิธีนี้ประมาณค่า “หลัก” ปัจจุบันของ quotient จาก “หลัก” 3 ตัวแรกของ dividend และ “หลัก” 2 ตัวแรกของ divisor
- การติดตั้งใช้งานนี้ช่วยประหยัดเวลาการหารได้ประมาณ 40,000ns ต่อครั้ง
- หาก divisor มีเพียงชังก์
u64เดียว ก็มีการทำกรณีพิเศษให้ใช้u128ทำ long division แบบตรงไปตรงมามากขึ้น - กรณีนี้เกิดบ่อยใน Miller-Rabin
-
การคูณ
- การคูณเร็วขึ้นราว 2 เท่า จากการจัดลำดับลูปใหม่เพื่อตัด BigInt สำหรับเก็บผลลัพธ์กลางออกไป
- มีการคำนวณจำนวนชังก์ที่ถูกใช้งานจริง แล้ววนลูปเฉพาะชังก์ที่ไม่เป็นศูนย์
- เนื่องจาก BigInt ส่วนใหญ่มักเก็บค่าที่ไม่เกิน 1024 บิต ทำให้พื้นที่ 2048 บิตครึ่งหนึ่งว่างอยู่บ่อยครั้ง
- แม้จะพิจารณา Karatsuba หรือการคูณแบบ FFT ด้วย แต่เห็นว่าซับซ้อนเกินไปสำหรับการเขียนเอง และการคูณปัจจุบันก็เร็วพอแล้ว
-
การปรับแต่งภายใน Miller-Rabin
- ใน Miller-Rabin จะเน้นลดจำนวนงานที่มีต้นทุนสูง
- แทน
x = mod_exp(x, 2, n)ด้วยx = (x * x) % nโดยตรง mod_exp()ตัวแรกถูกแทนด้วยเวอร์ชัน inline ที่ลดรูปลง เพื่อลด overhead จากการเรียกฟังก์ชัน- เพิ่ม
num.is_even()สำหรับตรวจเลขคู่แทนการคำนวณ% 2 - เปลี่ยน
d / 2เป็นd >>= 1 += 1,-= 1ถูกทำกรณีพิเศษเป็นincrease()และdecrease()- โดยเฉพาะ
is_even()และd >>= 1ให้ผลประหยัดเวลาราว 70,000ns ต่ออย่าง - ใน benchmark สุดท้าย เวอร์ชันปรับแต่งแบบชังก์
u64เร็วขึ้นมาก a * b: 842.32ns → 295.04nsa / bและa % b: 44440.12ns → 831.77nsa / 2: 75121.58ns → 60.89nsa % 2 == 0: 78400.87ns → 21.65nsa - 1: 103.15ns → 67.54ns
ตัวสร้างจำนวนเฉพาะ 1024 บิตเวอร์ชันสุดท้าย
- ฟังก์ชันสุดท้ายจะเริ่มจากอ่านเลขสุ่ม 1024 บิตจาก
/dev/urandom- เปิดบิตสูงสุดเพื่อรับประกันว่ามีขนาด 1024 บิต
- เปิดบิตต่ำสุดเพื่อรับประกันว่าเป็นเลขคี่
- หลังจากนั้นจะไม่อ่านเลขสุ่มใหม่ทุกครั้ง แต่เพิ่มค่าผู้สมัครทีละ
2เพื่อเลื่อนไปยังเลขคี่ถัดไปincrease_by_2()ส่วนใหญ่มักเป็นเพียงการบวกในชังก์u64ตัวเดียว
- ก่อนทำ Miller-Rabin จะลองหารด้วยรายการจำนวนเฉพาะขนาดเล็กก่อน
- ในโค้ดสุดท้ายใช้จำนวนเฉพาะขนาดเล็ก 1000 ตัวแรก
- จำนวนเฉพาะเล็กเหล่านี้อยู่ในชังก์
u64เดียว จึงใช้ประโยชน์จากกรณีพิเศษของการหารแบบชังก์เดียวที่เร็วกว่าได้
- ปัญหานี้จัดอยู่ในประเภท embarrassingly parallel เพราะไม่ต้องใช้หน่วยความจำร่วมกันหรือซิงก์ระหว่างเธรด
- เธรด CPU 16 ตัวจะต่างคนต่างค้นหาจำนวนเฉพาะ แล้วใช้ค่าจากเธรดที่ส่งผลลัพธ์มาได้ก่อน
- ตัวอย่างการรันสุดท้ายบันทึกเวลา elapsed ไว้ประมาณ 0.086 วินาที
- มีการแสดงการใช้ CPU ที่ 690%
- ค่าเฉลี่ยจากการรัน 100 ครั้งคือ
0.04109 ± 0.00307วินาที- โดยเฉลี่ยแล้วหาจำนวนเฉพาะ 1024 บิตได้ในราว 40ms
- การเรียก
prime_1024bit()แต่ละครั้งอาจแกว่งตั้งแต่ประมาณ 8ms ถึงราว 800ms ตามความสุ่ม - การทำงานแบบขนานช่วยลดความผันผวนด้วยการเลือกผลลัพธ์ที่เร็วที่สุด
โค้ดและข้อจำกัด
- โค้ดทั้งหมดและรีโพซิทอรีเผยแพร่อยู่ที่ github
- ลิงก์พูดคุยอยู่ที่ hackernews และ reddit
- งานติดตั้งใช้งานนี้ไม่ควรมองว่าปลอดภัยพอสำหรับการเข้ารหัสจริง และเป้าหมายก็ไม่ใช่การสร้างไลบรารีคริปโต แต่ใกล้เคียงกับ การเรียนรู้และการทดลองลงมือเขียน มากกว่า
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เคยมีคริปโตเคอร์เรนซีอยู่ไม่กี่ตัวที่ใช้การหาจำนวนเฉพาะขนาดใหญ่เป็นส่วนหนึ่งของฟังก์ชัน proof of work และเมื่อราว 8 ปีก่อน แค่มี implementation ของ การทดสอบความเป็นจำนวนเฉพาะ ที่เร็วมากก็ทำเงินได้พอสมควรแล้ว
ผมเคยเป็นผู้เขียนและผู้ดูแลซอฟต์แวร์ขุด riecoin อยู่พักหนึ่ง ไม่ค่อยแน่ใจว่าทำไมเหมือนกัน คงเพราะแค่ชอบจำนวนเฉพาะ
บทความนี้ตกหล่น Montgomery multiplication ซึ่งเป็นการปรับแต่งอันดับหนึ่งสำหรับการทดสอบจำนวนเฉพาะให้เร็ว: https://en.m.wikipedia.org/wiki/Montgomery_modular_multiplic...
สิ่งนี้เป็นพื้นฐานของ implementation การยกกำลังโมดูลาร์ความเร็วสูงที่ใช้งานได้จริง
Niall Emmart ซึ่งตอนนั้นอยู่ในแวดวงวิชาการ และเท่าที่รู้ตอนนี้อยู่ที่ Nvidia ได้เปิดเผยไลบรารี big integer บน GPU ที่เร็วสุด ๆ ชื่อ CGBN: https://github.com/NVlabs/CGBN
จนถึงตอนนี้ก็ยังเป็น implementation การยกกำลังโมดูลาร์แบบ batch ที่เร็วที่สุดเท่าที่ผมรู้จัก และถ้าขอชื่นชมแบบเนิร์ด ๆ สักหน่อย มันน่าทึ่งจนแทบลืมหายใจ
สักวันคงต้องเขียนเรื่องที่ผมใช้สิ่งนี้ครอบงำการผลิตคริปโตเคอร์เรนซีตัวเล็ก ๆ อยู่ราว 5 ปี และใน Python ก็มีการยกกำลังโมดูลาร์ที่ค่อนข้างดีสำหรับคำนวณ
x^y % mผ่านรูปแบบสามอาร์กิวเมนต์ของpow(x, y, m)ใช้สิ่งนี้แล้วจะสร้างการทดสอบจำนวนเฉพาะแบบ Fermat หรือ Miller-Rabin ได้ง่ายมากหากอยาก implement เอง และก็สนุกดีพอสมควร ถ้าไม่อยากทำเอง
mpz_probab_prime()ในไลบรารี gmp ก็ใช้ได้ดี แน่นอนว่า gmp เร็วกว่า แต่เวลาลองเล่นกับจำนวนเฉพาะขนาดใหญ่ ความสนุกของ Fermat test สองบรรทัดก็เอาชนะได้ยากมันใกล้เคียงกับการยกกำลังโมดูลาร์แบบ batch มาก ต่างกันตรงที่ทำงานบน elliptic curve ไม่ใช่โมดูโลจำนวนเฉพาะ ผมมองว่าเป็นผลสืบเนื่องจากงาน CGBN
ปีที่แล้วเขาได้บรรยายดี ๆ ในสัมมนามื้อกลางวันด้านวิทยาการเข้ารหัสของ Stanford และมีทั้งสไลด์กับวิดีโอบันทึกออนไลน์
https://cbr.stanford.edu/seminarTalks/slides_20230526_niall_...
https://www.youtube.com/watch?v=KAWlySN7Hm8
อยากรู้ว่าเป็นแค่ความคิดคลุมเครือว่า cryptography ยังไงก็ใช้จำนวนเฉพาะอยู่แล้ว แต่ไม่รู้ว่าใช้เมื่อไรและทำไม หรือมีเหตุผลที่ลึกกว่านั้น
pow(x,e,mod)นี่แหละถ้ามีขอบเขตค่าสูงสุดที่กำหนดไว้ การทำให้ Miller-Rabin กลายเป็นแบบ deterministic ในทางปฏิบัตินั้นทำได้ง่าย
แค่เลือกฐานที่พิสูจน์แล้วว่าสามารถคัดกรอง pseudoprime ทั้งหมดภายในช่วงนั้นออกไปพร้อมกันได้
รายการก็ไม่ได้ยาวขึ้นด้วย Miller-Rabin ทรงพลังจริง ๆ
หาในออนไลน์แล้วไม่เจอคำตอบ
แค่ inline assembly หนึ่งบรรทัดก็ทำให้การคูณ big integer แบบประถมง่ายขึ้น: https://github.com/jcalvinowens/toy-rsa/blob/master/bfi.c#L4...
ถ้าย้อนเวลากลับไปเปลี่ยนภาษา C ได้อย่างเดียว ผมอยากใส่แนวคิด extended multiplication เข้าไป น่าเสียดายที่ Rust ก็ไม่มีเหมือนกัน ทั้งที่ฮาร์ดแวร์รองรับแทบทุกที่ Cortex M0 ไม่มีแม้แต่การหาร แต่มี extended multiplication
โค้ดนี้มาจาก implementation RSA ของเล่นที่เขียนไว้นานแล้วและหน้าตาแย่มาก: https://github.com/jcalvinowens/toy-rsa
เหตุผลที่แค่ Fermat test ก็เอาอยู่คือ ถ้าจำนวนเฉพาะเหล่านั้นไม่ได้เป็นจำนวนเฉพาะจริง อัลกอริทึมก็จะไม่ทำงาน Fermat test เร็ว และการเข้ารหัส/ถอดรหัสหนึ่งครั้งก็จะกำจัดความเป็นไปได้ที่เล็กสุด ๆ ว่าตัวใดตัวหนึ่งเป็น Fermat liar ได้
อย่างไรก็ตาม ผมไม่รู้ว่าจะพิสูจน์ได้ไหมว่าไม่มีคีย์คู่ RSA ที่สามารถเข้ารหัส/ถอดรหัสข้อความได้สำเร็จด้วยค่า P/Q ที่ไม่ใช่จำนวนเฉพาะ ใน implementation จริงย่อมไม่ใช่วิธีมาตรฐานแน่นอน แต่ผมไม่เคยหาคำตอบได้
ใน C23 มีการเพิ่มชนิด
_BitInt(N)และเช่นสามารถใช้_BitInt(1024)เป็นชนิดขนาด 128 ไบต์ได้แต่การรองรับของคอมไพเลอร์ยังจำกัด ใน Clang ถ้าจะอนุญาตให้ N มากกว่า 128 ให้ส่งแฟล็ก
-fexperimental-max-bitint-width=Nได้ ถ้า N มากกว่า 128 แล้วหาร_BitInt(N)คอมไพเลอร์จะ crash ไปเลย แต่+,-,*ทำงานตามคาดมี builtin
@mulWithOverflowที่คืนค่าผลลัพธ์พร้อมบิต overflow และจำนวนเต็มมีถึง(u|i)65535ขึ้นอยู่กับงานที่ทำ คุณอาจตรวจจับ overflow แล้วเลื่อนขึ้นเป็นชนิดที่ใหญ่กว่า หรือเลื่อนขึ้นก่อนแล้วค่อยตัดออกแบบเลือกได้
อีกทั้งยังรองรับ saturating multiplication ด้วยตัวดำเนินการแยก
*|และ wrapping multiplication ด้วย*%ใช้ได้เมื่อต้องการ semantics แบบนี้ ส่วน overflow อื่น ๆ ถือเป็น undefined behavior ที่อยู่ภายใต้ safety check ดังนั้นในโหมด build แบบ Debug และ ReleaseSafe จะเกิด panicpและqเป็น Carmichael number ที่เป็น coprime กัน RSA ก็ยังสามารถเข้ารหัสและถอดรหัสข้อความได้สำเร็จแต่
p*qจะมี prime factor ที่เล็กกว่า ทำให้แยกตัวประกอบได้ง่ายขึ้น ความปลอดภัยจึงลดลงสงสัยว่างานนี้ใช้เวลานานแค่ไหน
ตอนเป็นโปรเจกต์วิจัยปริญญาตรี ผมเคยทำการคูณจำนวนเต็มขนาดใหญ่ ซึ่งใช้เวลาเกือบสองเทอม ผม implement ทั้ง Karatsuba, Toom-Cook, FFT เชิงซ้อน, NTT หลายแบบ และ Schonhage-Strassen
จำนวนเฉพาะแทบจะเหมือนเวทมนตร์ทางคณิตศาสตร์ สำหรับคนที่สนใจ A Friendly Introduction to Number Theory ของ Silverman เป็นหนังสือคณิตศาสตร์ที่ยอดเยี่ยม
อนึ่ง ลิงก์ในหน้าดูเหมือนจะเป็น
4025051ไม่ใช่40250519เป็นบทความที่ดี ผมเองก็เพิ่งเขียน โค้ด big integer เองอยู่บ้างสำหรับเวอร์ชันแรก ๆ ของ [0] และยังจำได้ว่าการแปลงคำอธิบายระดับสูงใน论文คณิตศาสตร์ให้กลายเป็นการคำนวณจริงนั้นน่าหงุดหงิดแค่ไหน
แต่มีข้อท้วงเล็กน้อย
ถ้าใช้ช่วงทั้งหมดของ
u64ตัวเลขจะไม่ได้อยู่ในฐาน2^64-1แต่เป็น ฐาน 2^64 แต่ละ word มีช่วงตั้งแต่ 0 ถึง2^64-1เหมือนกับที่แต่ละหลักของเลขฐานสิบมีค่าตั้งแต่ 0 ถึง 9[0] https://github.com/LegionMammal978/bigfoot-sim
ถ้าเหมือน optimization สุดท้าย คือเมื่อเจอความล้มเหลวแล้วไม่สร้างเลขสุ่มใหม่ แต่เพิ่มค่าตัวเลขทีละ 2 จะทำให้ความปลอดภัยเสียไปเล็กน้อย
เพราะจำนวนเฉพาะไม่ได้กระจายตัวอย่างสม่ำเสมอ จึงจะมี bias ไปทางจำนวนเฉพาะที่อยู่ถัดจาก ช่องว่างระหว่างจำนวนเฉพาะ ขนาดใหญ่
มันเป็นการแลกระหว่างความเร็วในการรันกับความสุ่มของจำนวนเฉพาะ และผมเลือกความเร็ว โดยเห็นว่าถ้าให้ 16 เธรดเริ่มจากเลขสุ่มของตัวเองแล้วแข่งกันหาจำนวนเฉพาะ ก็น่าจะเพิ่มความสุ่มได้เพียงพอ
ถ้าต้องการความสุ่มมากกว่าความเร็ว การเปลี่ยน
+=2เป็นการเรียกrng()ก็เป็นการแก้ที่ง่ายเป็นบทความที่ดีและเขียนได้ดี
ผู้เขียนน่าจะหมายถึง base-256 ไม่ใช่
base-255ตัวเลขขนาด 1–2KB เพียงไม่กี่ตัวน่าจะใส่ใน L1 cache ได้สบาย และต่อให้ไม่พอ ก็ยังมี L2 cache ขนาดระดับเมกะไบต์ขึ้นไปที่มีเวลาเข้าถึงราว 3ns
ในบทความบอกว่าน่าจะต้องรอการอ่าน/เขียน RAM เพราะ L1 cache miss แต่หลังจากนั้นก็ไม่ได้กลับมาพูดถึงประเด็นนี้อีก
อีกอย่าง นี่ครอบคลุมแค่การสร้างจำนวนเฉพาะ จึงหลบกับดักส่วนใหญ่ของ RSA ไปได้ และ
urandomก็ควรปลอดภัย ถ้าโค้ดทำงานถูกต้อง ก็ไม่ได้มีจุดที่จะผิดพลาดใหญ่ ๆ มากนักRSA มีประเด็นเกี่ยวกับ จำนวนเฉพาะอ่อนแอ ที่ควรหลีกเลี่ยงอยู่บ้าง แต่ผมไม่รู้ว่ามันพบบ่อยพอจะเป็นปัญหาจริงในที่นี้หรือไม่
ทำให้นึกถึงโปรเจกต์ตอนปีหนึ่งมหาวิทยาลัยเมื่อหลายสิบปีก่อน
เพื่อนที่เป็นพาร์ตเนอร์โปรเจกต์ของผม ซึ่งต่อมากลายเป็นตัวแทนผู้สำเร็จการศึกษา เป็นคนเสนอไอเดียและ implement คณิตศาสตร์หลัก ๆ เป้าหมายคือสร้าง การเข้ารหัส RSA 4096 บิต
ผมจำได้ว่าการสร้างจำนวนเฉพาะใน implementation สุดท้ายนั้นช้ามาก ใช้เวลาสร้างประมาณ 20 นาทีบนเวิร์กสเตชัน PA-RISC
เพื่อนคนนี้เป็นสายคณิตศาสตร์ตัวจริง และหลังจบโปรเจกต์ก็ยังปรับแต่งโค้ดต่อ ผมยังจำภาพที่เขาอ่าน论文เรื่องการทดสอบจำนวนเฉพาะและการ implement คณิตศาสตร์ big integer ได้
เช่น มีการปรับปรุงครั้งใหญ่มากเมื่อในขั้นตอนการคูณประกอบ ถ้าค่าใดค่าหนึ่งเป็น 0 ก็ข้ามการคูณแล้วให้ผลลัพธ์เป็น 0 ไปเลย
ไม่อย่างนั้นก็ต้องรอนาน หรือไม่ก็ยอมเสียสละความปลอดภัยที่ควรทนไปถึงอนาคต
เข้าใจว่าทำไมต้องตั้งบิตต่ำสุดเป็น 1 เพราะเลขคู่ไม่มีทางเป็นจำนวนเฉพาะ แน่นอนว่ายกเว้น 2
แต่ไม่เข้าใจว่าทำไมต้องตั้งบิตสูงสุดเป็น 1 ด้วย ผมไม่ได้รู้เรื่องจำนวนเฉพาะหรือการเข้ารหัสดีนัก แต่มันดูเหมือนยอมทิ้ง เอนโทรปี 1 บิต โดยไม่จำเป็น ผมพลาดอะไรไปหรือเปล่า?
การ encode ไบต์แบบความยาวแปรผันอาจสร้างปัญหาในการแลกเปลี่ยนข้อมูลระหว่างซอฟต์แวร์ต่าง ๆ ได้ ถ้าสเปกไม่ชัดเจนมากและไม่ได้ทดสอบมาดี
ดูปัญหาที่เกิดกับ DHE แบบอิง RSA ตอน public key ของเซิร์ฟเวอร์มี 0 นำหน้าได้
ถ้าหลักแรกเป็น 0 ก็ไม่ใช่ตัวเลขสองหลัก
ขอเสริมอีกอย่าง ใน RSA เราคูณจำนวนเฉพาะสองตัว ถ้าตัวหนึ่งเป็น 1024 บิต อีกตัวถ้าจำไม่ผิดเป็นราว 200 บิตก็อาจถึงจำนวนบิตเอนโทรปีที่คีย์ต้องการแล้ว
ดังนั้นการให้จำนวนเฉพาะทั้งสองตัวเป็น 1024 บิต ก็ยังมี margin เพิ่มอีกเล็กน้อย
แบบนี้น่าจะปลอดภัยกว่าการที่มีคนขอจำนวนเฉพาะ 1024 บิต แล้วต้องมาคิดว่าจำนวนเฉพาะ 1020 บิตพอใช้ได้ไหม เหมือนปกติเราไม่ถือว่า
00042เป็นตัวเลข 5 หลักในทางเทคนิค ตัวเลือกที่เหมาะที่สุดอาจต่างกันตามว่าจะเอาไปใช้ตรงไหน แต่แนวทางในบทความดูเป็นค่าเริ่มต้นที่ปลอดภัยกว่า