Skip to content

Instantly share code, notes, and snippets.

@polarathene
Created December 14, 2020 05:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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 Oct 2, 2021

The linked NIST document received an update in 2021Q2, the referenced page 119 is now 121.

  • (64/9)^(1/3) == cbrt(64/9), that is for base^power, using a power of 1/3 is equivalent result to the cube root.
  • The square root can be achieved in the same manner with a power of 1/2.
  • Likewise, this helped me understand 2/3, as the original algorithm uses cbrt(a * b * c), where c == ln( ln( n ) )^2 == x^2. These two are equivalent: cbrt( x^2 ) == x^(2/3).
  • Turns out this is basic math knowledge called Fractional Exponents 😅

Comparing the GNFS algorithm (with log2() for bits representation) to the NIST variant: log2( e^(a * b * c) ) == ( (a * b * c) / log(2) ) => log2(e^x) == (x / log(2)), the only difference with NIST is it also subtracts 4.69 (or separate from the original symmetric bits result subtract: 4.69 / log(2) == log2(exp(4.69)) == ~6.766 bits). That is what contributes to the delta between GNFS and NIST.

This answer details that the value would have been defined to adjust the result from 86 symmetric bits for 1024-bit, to the desired 80 symmetric bits. While insightful answers here note that GNFS accuracy for such a symmetric strength equivalence is imprecise, so lowering results by a few bits adds some safety margin, and they round to the nearest byte (8-bit) boundary after the adjustment, with a little wiggle room (eg RSA-7680) to align with existing symmetric bit lengths like 192-bit.

Adding another resource, the NFS algorithm was cited in this document back in 2004, focused on determining symmetric strength. This notes like others that Diffie-Hellman is said to be a little bit stronger than the equivalent RSA size, noting 20-64x more difficult (4.3-6 bits), which aligns with rough estimates stated elsewhere (eg: 1024-bit DH being roughly 1200-bit RSA). It's possible that the offset NIST adds is related to this. Section 2.2 of the RFC 3766 document also notes the GNFS algorithm is calculating on the basis of single/simple operations as opposed to elementary operations/instructions that would actually be involved (in regards to relative strength, which this is often used for comparison, it's a non-issue), they shift the focus to MIPS years (MYs) and note that actual factorings at the time had proven to be far less effort than naively assumed.

Relative symmetric strength roughly holds up when comparing results with equivalent CPU core years.


# Alternative notes on calculation, breakdown, alternative formats, and an example that can be copy/pasted into KRunner or similar.

N = 1024 # bit length
a = 64/9 # cbrt(a) == ~1.923
b = log(2^N)
c = log(log(2^N))
d = 4.69 # NIST adjusted offset, related: exp(d) == 2^(d / log(2))

# (a * b * c) below can be expressed as any:
( cbrt(a) * cbrt(b) * cbrt(c^2) ) == cbrt(a * b * c^2) == ( cbrt(a) * b^(1/3) * c^(2/3) )

# exp(x) == e^(x)
# `/ exp(d)` or `- d` reduce output bits by `4.69 / log(2) == log2(exp(4.69)) == ~6.766`

# All equivalent:
log2(exp( (a * b * c) - d ))
log2( exp(a * b * c) / exp(d) )
log2( exp(a * b * c) ) - ( d / log(2) )
( (a * b * c) / log(2) ) - ( d / log(2) )
( (a * b * c) - d ) / log(2)

# Example - 1024-bit asymmetric to symmetric bit strength estimate:
(cbrt( (64/9) * log(2^1024) * log(log(2^1024))^2 ) - 4.69) / log(2) = ~80
log2(exp(cbrt( (64/9) * log(2^1024) * log(log(2^1024))^2 ))) - log2(exp(4.69)) = ~80

Additional calculations to try match MIPS with the equation to the results from the RSA challenge results, by mixing in MIPS-year into the calculation as explained in RFC 3766 - Section 2.2:

# 1 year in seconds multiplied by 1 million for 1 MIPS-year (MY)
0.02 / ((60*60*24*365) * 1e6) = ~6.34e-16

# Feb (2000 MY, 61.1 bits) and Aug (8000 MY, 63.9 bits) 1999,
# Symmetric strength delta is 7x `2^63.9 / 2^61.1`. MY without adjustment: 1581 vs 11136:
exp(cbrt( (64/9) * log(2^463) * log(log(2^463))^2 )) * (6.34e-16 / 2^-0.34) = ~2000
exp(cbrt( (64/9) * log(2^512) * log(log(2^512))^2 )) * (6.34e-16 / 2^0.48) = ~8000

# 2009 (66M MY, 76.5 bits) - '2.2 GHz AMD Opteron' core years,
# Naively adjusted equivalent MIPS-years: `2000 * 2^15 == 65 536 000 MY`.
# Adjustment of MY is roughly in line with symmetric delta: `76.5 - 61.1 = 15.4` and `76.5 - 63.9 = 12.6`.
(exp(cbrt( (64/9) * log(2^768) * log(log(2^768))^2 )) * (6.34e-16 / 2^15)) = ~2000

# 2019 (154M MY, 77.7 bits) & 2020 (417M MY, 79.1 bits) - '2.1 GHz Xeon Gold 6130' core years,
# Symmetric strength delta 2.64x `2^79.1 / 2^77.7`.
# MY adjustment based on core years reported, roughly aligns with relative difficulty of symmetric delta,
# Entry on wikipedia notes algorithm improvements for 3-4 speed-up (about 2 bits reduction).
(exp(cbrt( (64/9) * log(2^795) * log(log(2^795))^2 )) * (6.34e-16 / 2^17.38)) = ~900
(exp(cbrt( (64/9) * log(2^829) * log(log(2^829))^2 )) * (6.34e-16 / 2^17.237)) = ~2700


# Attempts to calculate based on reference MIPS performance at: https://en.wikipedia.org/wiki/Instructions_per_second
# In addition to MIPS adds varying speed-up multiplier, this would presumably normally modify the `0.02` value instead.

# 2009 AMD Opteron estimated MIPS based on AMD MIPS around 2005: ~7000
( (exp(cbrt( (64/9) * log(2^768) * log(log(2^768))^2 )) * ((0.02) / (60*60*24*365 * 1e6)) ) = 68M MY
( (exp(cbrt( (64/9) * log(2^768) * log(log(2^768))^2 )) * ((0.02) / (60*60*24*365 * 1e6)) ) / (7000 * 5) = ~2000
( (exp(cbrt( (64/9) * log(2^768) * log(log(2^768))^2 )) * ((0.02) / (60*60*24*365 * 1e6)) ) / (10000 * 3.4) = ~2000

# 2019 & 2020 Xeon Gold 6130 estimated MIPS based on Skylake 6950X: 32000
( (exp(cbrt( (64/9) * log(2^829) * log(log(2^829))^2 )) * ((0.02) / (60*60*24*365 * 1e6)) ) / (32000 * 4.8) = ~2700

# AMD Ryzen ThreadRipper 3990X approx 37000 MIPS per core, RSA-768 in 370 core years:
( (exp(cbrt( (64/9) * log(2^768) * log(log(2^768))^2 )) * ((0.02) / (60*60*24*365 * 1e6)) ) / (37000 * 5) = ~370 years
# Compared to RSA 512-bit
( (exp(cbrt( (64/9) * log(2^512) * log(log(2^512))^2 )) * ((0.02) / (60*60*24 * 1e6)) ) / (37000 * 5) = 22 days
# With all 64 cores, adjusted speed-up of 3x and 5x:
( (exp(cbrt( (64/9) * log(2^512) * log(log(2^512))^2 )) * ((0.02) / (60*60 * 1e6)) ) / (37000*64 * 3) = 13.74 hours
( (exp(cbrt( (64/9) * log(2^512) * log(log(2^512))^2 )) * ((0.02) / (60*60 * 1e6)) ) / (37000*64 * 5) = 8.24 hours

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