Skip to content

Instantly share code, notes, and snippets.

@polarathene
Created December 14, 2020 05:33
Show Gist options
  • Save polarathene/9e3cd791091dc2bd075a2ec4cf793aaa to your computer and use it in GitHub Desktop.
Save polarathene/9e3cd791091dc2bd075a2ec4cf793aaa to your computer and use it in GitHub Desktop.
crypto.stackexchange - RSA security strength in relation to modulus size

Answer posted to: https://crypto.stackexchange.com/questions/8687/security-strength-of-rsa-in-relation-with-the-modulus-size

A moderator deleted it about a month later on the basis it wouldn't be useful to anyone due to mostly being a code port.

Making it available to public here in case it'd be useful for anyone else. Running JS or Rust is possibly more easily accessible than the two options in answers provided.


For anyone else curious and wanting to run the math via a language that is more available/accessible (eg a REPL via a web browser), I ported the answer by Charles to Javascript (ES6) below:

const a = 1/3
const b = 2/3
const l = (x) => Math.log(x)
const e = (x) => Math.exp(x)

function to_symmetric_bits(asymmetric_bits) {
  const t = asymmetric_bits * l(2)
  const m = l(t)
  const n = e( l(m) * b )
  const o = e( l(t) * a )
  const p = (1.923 * o * n - 4.69) / l(2)

  return p
}

const sizes = [256, 512, 1024, 2048, 3072, 4096, 7680, 8192, 15360, 16384]

console.log(`RSA Keylength to equivalent 'Bits of Security':`)
sizes.forEach((size) => {
  console.log(`${size} == ${to_symmetric_bits(size)}`)
})

After which, I looked at the official cited NIST source on page 119 and noted that the math while valid seemed not to match the equation... So here's a more direct variant that should be easy to follow when comparing to the linked equation:

function to_symmetric_bits(keylength) {
  const a = keylength * Math.log(2) // L * ln(2)
  const b = Math.pow(Math.log(a), 2) // ln(L * ln(2))^2

  // Top half (numerator) / ln(2) == x
  const numerator = 1.923 * Math.cbrt(a) * Math.cbrt(b) - 4.69
  const x = numerator / Math.log(2)

  return x
}

const sizes = [256, 512, 1024, 2048, 3072, 4096, 7680, 8192, 15360, 16384]

console.log(`RSA Keylength to equivalent 'Bits of Security':`)
sizes.forEach((size) => {
  console.log(`${size} == ${to_symmetric_bits(size)}`)
})

Output:

RSA Keylength to equivalent 'Bits of Security':
256 == 39.8983534444018
512 == 57.16312330388321
1024 == 79.99990535893299
2048 == 110.11760837749868
3072 == 131.97008244496166
4096 == 149.73076030163335
7680 == 196.25255668822675
8192 == 201.70630874279217
15360 == 262.6186131379134
16384 == 269.7522498627511

The document on page 119 also cites after the equation:

If the lengths of the Diffie-Hellman public and private keys are L and N, correspondingly, then the length y of a symmetric key of approximately the same strength can be computed as: y = min(x, N / 2) where x is computed as in formula (1) above

N values are shared for some key lengths at the top of page 119, when divided by 2, you end up with the 112, 128, 192, 256 values. Which for the output shared above would result in 110(2048), 128(3072), 192(7680), 256(15360).

The document referenced has an initial release date of 2003, and was last updated in 2020. The equation doesn't appear to have changed from what has been shown in this thread from answers years ago however.


I didn't notice originally, but Charles initially ported the GNFS algorithm from Reid, and for whatever reason later modified it to adjust to the NIST algorithm. That is there shouldn't have been any a, b fractions or Math.exp()? The GNFS equation also has Math.cbrt(64/9) which is where the 1.923 value came from.

The following is JS equivalent, but you won't have success with it due to JS numbers being larger than supported (infinity starts from 2^1024):

// log2(exp(cbrt(64/9) * log(2^1024)^(1/3) * log(log(2^1024))^(2/3)))
function to_symmetric_bits(keylength) {
  const n = Math.pow(2, keylength)

  const a = Math.cbrt(64/9)
  const b = Math.pow(Math.log(n),1/3)
  const c = Math.pow(Math.log(Math.log(n)), 2/3)

  const x = Math.log2(Math.exp(a * b * c))

  return x
}

Turns out t var from Charles approach produces the same desired output for the n value in my code above. This avoids the ridiculously large number becoming infinity (which would affect most languages float numbers? Not just JS, I verified with Rust f64 type):

function to_symmetric_bits(keylength) {
  const n = keylength * Math.log(2)
 
  const a = Math.cbrt(64/9)
  const b = Math.pow(n, 1/3)
  const c = Math.pow(Math.log(n), 2/3)

  const x = Math.log2(Math.exp(a * b * c))

  return x
}

Rust:

fn main() {
    let sizes: Vec<i32> = vec![256, 512, 1024, 2048, 3072, 4096, 7680, 8192];
    
    println!("RSA Keylength to equivalent 'Bits of Security':");
    for size in sizes.into_iter() {
        println!("{} == {}", size, to_symmetric_bits(size as f64));
    }
}

fn to_symmetric_bits(keylength: f64) -> f64 {
    let n = (2_f64).ln() * keylength;

    let a = f64::cbrt(64.0/9.0);
    let b = n.powf(1.0/3.0);
    let c = n.ln().powf(2.0/3.0);

    let x = ((a * b * c).exp()).log2();

    x
}

For anyone interested of the output for 256-bit and 512-bit RSA:

256 == 46.664579283290124
512 == 63.92934399904213
@polarathene
Copy link
Author

polarathene commented Nov 28, 2024

The linked NIST document received an update in 2023Q4, the referenced page 121 is now 126.

image


Revised Rust snippet to include a port of the original JS algorithm (with comment added to NIST variant to more clearly show the equivalence in results), along with a rounded version to demonstrate how it aligns with the table:

fn main() {
    let sizes: Vec<i32> = vec![256, 512, 1024, 2048, 3072, 4096, 7680, 8192];
    
    println!("NIST (rounded) - RSA Keylength to equivalent 'Bits of Security':");
    for size in &sizes {
        println!("{} == {}", size, to_symmetric_bits(*size as f64));
    }

    println!("");
    println!("NIST - RSA Keylength to equivalent 'Bits of Security':");
    for size in &sizes {
        println!("{} == {}", size, to_symmetric_bits_nist(*size as f64));
    }

    println!("");
    println!("GNFS - RSA Keylength to equivalent 'Bits of Security':");
    for size in &sizes {
        println!("{} == {}", size, to_symmetric_bits_gnfs(*size as f64));
    }
}

/*
NIST (rounded) - RSA Keylength to equivalent 'Bits of Security':
256 == 40
512 == 64
1024 == 80
2048 == 112
3072 == 136
4096 == 152
7680 == 200
8192 == 208

NIST - RSA Keylength to equivalent 'Bits of Security':
256 == 39.89835344440179
512 == 57.1631233038832
1024 == 79.99990535893298
2048 == 110.11760837749866
3072 == 131.97008244496166
4096 == 149.7307603016333
7680 == 196.25255668822675
8192 == 201.70630874279217

GNFS - RSA Keylength to equivalent 'Bits of Security':
256 == 46.664579283290124
512 == 63.92934399904213
1024 == 86.76611925028118
2048 == 116.8838132958159
3072 == 138.73628085272506
4096 == 156.49695341792042
7680 == 203.01873594417663
8192 == 208.47248637389424
*/

// Rounds the result to the nearest byte in bits:
// This roughly matches the NIST table results, except for 7680 => 192
fn to_symmetric_bits(keylength: f64) -> u64 {
    let n = to_symmetric_bits_nist(keylength);
    let bits = (n / 8.0).round() as u64 * 8;

    bits
}

// Original algorithm with the NIST adjustment:
fn to_symmetric_bits_nist(keylength: f64) -> f64 {
  let ln2 = (2_f64).ln();
  let a = keylength * ln2; // L * ln(2)
  let b = a.ln().powf(2.0); // ln(L * ln(2))^2

  // Top half (numerator) / ln(2) == x
  let numerator = 1.923 * f64::cbrt(a) * f64::cbrt(b) - 4.69;
  let bits = numerator / ln2;
  
  bits
}

// GNFS algorithm without bias applied:
fn to_symmetric_bits_gnfs(keylength: f64) -> f64 {
    let n = (2_f64).ln() * keylength;

    let a = f64::cbrt(64.0/9.0);
    let b = n.powf(1.0/3.0);
    let c = n.ln().powf(2.0/3.0);

    // The equivalent change to get the NIST output:
    // let bits = (a * b * c - 4.69).exp().log2();
    let bits = (a * b * c).exp().log2();
    
    bits
}

The March 2006 document noted 80-bit for 1024-bit modulus, which the current iteration no longer mentions. Worth referencing to further demonstrate the math used by NIST along with rounding:

Page 27

image

Page 63

image

Perhaps rounding occurred a little earlier, or they've manually rounded it for the 7680-bit to 192-bit symmetric entry, which as we can see was calculated at slightly above 196 bits so it rounded up instead of down. 192-bit is preferrable to NIST and those seeking security advice since it aligns with the more familiar 192-bit in AES (1 of the 2 additional levels of security requested when standardizing AES) and is a multiple of 64.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment