Introduction
Public key cryptography, pioneered by the RSA algorithm in 1977, revolutionized digital security by solving one of cryptography’s fundamental challenges: how to securely communicate without first sharing a secret key. Named after its inventors Ron Rivest, Adi Shamir, and Leonard Adleman, RSA encryption enables secure communications, digital signatures, and authentication in a world where parties have never met and cannot trust intermediaries.
Today, RSA forms the backbone of internet security, protecting everything from email communications and online banking to software distribution and cryptocurrency transactions. Understanding RSA cryptography is essential for security professionals, developers implementing authentication systems, and anyone building applications that require secure data exchange or identity verification.
This comprehensive guide explores RSA’s mathematical foundations, practical implementation strategies, key management best practices, and real-world applications. Whether you’re implementing SSH authentication, creating digital signatures, or building a complete Public Key Infrastructure (PKI), this knowledge will guide you through secure RSA deployment. Our RSA Key Generator tool demonstrates these concepts in action, providing hands-on experience with professional-grade key generation.
Background and Mathematical Foundations
The Problem RSA Solves
Before public key cryptography, all encryption was symmetric - the same key encrypted and decrypted messages. This created a fundamental challenge: how do two parties establish a shared secret when communicating over an insecure channel? Meeting in person to exchange keys didn’t scale, and sending keys electronically risked interception.
The Symmetric Key Problem:
Alice wants to send encrypted message to Bob
Traditional approach (symmetric):
1. Alice and Bob must first meet to exchange secret key
2. Or send key over insecure channel (vulnerable to interception)
3. Single key compromise exposes all communications
4. Scaling problem: n users require n(n-1)/2 shared keys
Public key solution:
1. Bob publishes public key (can be shared openly)
2. Alice encrypts with Bob's public key
3. Only Bob's private key can decrypt
4. No prior secure key exchange required
5. Scaling: n users require only 2n keys (public + private each)
How RSA Works: The Mathematics
RSA security relies on the mathematical difficulty of factoring large composite numbers into their prime factors.
Key Generation Process:
Step 1: Select two large prime numbers (p and q)
- Example: p = 61, q = 53 (real RSA uses 300+ digit primes)
Step 2: Compute n = p × q
- n = 61 × 53 = 3233
- n is the modulus, determines key strength
Step 3: Compute φ(n) = (p - 1) × (q - 1)
- φ(3233) = 60 × 52 = 3120
- φ (Euler's totient function)
Step 4: Choose public exponent e
- Typically e = 65537 (0x10001 in hex)
- Must be coprime with φ(n): gcd(e, φ(n)) = 1
Step 5: Compute private exponent d
- d ≡ e^(-1) mod φ(n)
- d × e ≡ 1 mod φ(n)
- Example: d = 2753
Result:
Public Key: (n, e) = (3233, 65537)
Private Key: (n, d) = (3233, 2753)
Encryption Process:
Message: m (must be < n)
Ciphertext: c = m^e mod n
Example: Encrypt m = 123
c = 123^65537 mod 3233 = 855
Decryption Process:
Ciphertext: c
Plaintext: m = c^d mod n
Example: Decrypt c = 855
m = 855^2753 mod 3233 = 123 ✓
Why It Works:
The RSA algorithm relies on Euler’s theorem and the Chinese Remainder Theorem:
- Encryption then decryption: (m^e)^d ≡ m^(ed) ≡ m mod n
- Since ed ≡ 1 mod φ(n), we have m^(ed) = m^(1 + kφ(n)) = m × (m^φ(n))^k ≡ m mod n
Security Foundation:
Knowing the public key (n, e), an attacker needs to compute the private exponent d. This requires:
- Factoring n into p and q (hard problem)
- Computing φ(n) = (p-1)(q-1)
- Finding d = e^(-1) mod φ(n)
For 2048-bit keys (617-digit numbers), factoring is computationally infeasible with current technology.
RSA Security Strength
Key Size vs. Security:
| Key Size | Decimal Digits | Security Bits | Comparable AES | Estimated Security Lifespan |
|---|---|---|---|---|
| 1024-bit | 308 digits | ~80 bits | AES-80 | ❌ Deprecated (broken 2010) |
| 2048-bit | 617 digits | ~112 bits | AES-112 | ✅ Secure through 2030 |
| 3072-bit | 925 digits | ~128 bits | AES-128 | ✅ Secure through 2040 |
| 4096-bit | 1234 digits | ~152 bits | AES-192 | ✅ Secure beyond 2050 |
Attack Complexity:
The best known factoring algorithm is the General Number Field Sieve (GNFS):
- Time complexity: exp((64/9)^(1/3) × (log n)^(1/3) × (log log n)^(2/3))
- 2048-bit RSA: ~2^112 operations (infeasible)
- 3072-bit RSA: ~2^128 operations (impossible with current computing)
Known Attacks and Mitigations:
-
Factorization Attacks:
- Direct factoring of n into p and q
- Mitigation: Use sufficiently large key sizes (2048+ bits)
-
Timing Attacks:
- Analyze decryption time to deduce private key
- Mitigation: Constant-time algorithms, blinding
-
Padding Oracle Attacks:
- Exploit vulnerabilities in padding schemes
- Mitigation: Use OAEP padding, not PKCS#1 v1.5
-
Weak Prime Selection:
- Using primes with special properties
- Mitigation: Proper random prime generation
-
Low Private Exponent Attack:
- If d is small, can be found efficiently
- Mitigation: Ensure d is sufficiently large (automatic in proper implementations)
RSA vs. Other Asymmetric Algorithms
Elliptic Curve Cryptography (ECC):
- Smaller keys for equivalent security (256-bit ECC ≈ 3072-bit RSA)
- Faster operations
- Less mature, fewer security audits
- Patent concerns (historically)
DSA (Digital Signature Algorithm):
- Designed specifically for signatures (not encryption)
- Smaller signatures than RSA
- Less flexible than RSA
ElGamal:
- Based on discrete logarithm problem
- Ciphertext doubles plaintext size
- Less widely adopted than RSA
Post-Quantum Algorithms (NIST standardization 2024):
- Lattice-based (CRYSTALS-Kyber, CRYSTALS-Dilithium)
- Hash-based (SPHINCS+)
- Resistant to quantum computer attacks
- Larger key sizes and signatures
- Future replacement for RSA
Implementation Workflows
Secure RSA Key Generation
Generating secure RSA keys requires careful attention to prime selection, randomness, and parameter validation.
Step 1: Prime Number Generation
class RSAKeyGenerator {
constructor(keySize = 2048) {
this.keySize = keySize;
this.publicExponent = 65537; // Standard value
}
// Generate cryptographically secure prime
async generatePrime(bitLength) {
while (true) {
// Generate random odd number
const candidate = this.generateRandomOdd(bitLength);
// Test primality
if (await this.isProbablyPrime(candidate)) {
return candidate;
}
}
}
generateRandomOdd(bitLength) {
const bytes = new Uint8Array(bitLength / 8);
crypto.getRandomValues(bytes);
// Ensure highest bit is set (full bit length)
bytes[0] |= 0x80;
// Ensure number is odd
bytes[bytes.length - 1] |= 0x01;
return this.bytesToBigInt(bytes);
}
// Miller-Rabin primality test
async isProbablyPrime(n, rounds = 40) {
// Small prime checks
const smallPrimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37];
for (const prime of smallPrimes) {
if (n % BigInt(prime) === 0n) {
return n === BigInt(prime);
}
}
// Miller-Rabin test
return this.millerRabinTest(n, rounds);
}
millerRabinTest(n, rounds) {
// Write n-1 as 2^r × d
let d = n - 1n;
let r = 0;
while (d % 2n === 0n) {
d /= 2n;
r++;
}
// Perform rounds of testing
for (let i = 0; i < rounds; i++) {
const a = this.randomRange(2n, n - 2n);
let x = this.modPow(a, d, n);
if (x === 1n || x === n - 1n) continue;
let continueOuter = false;
for (let j = 0; j < r - 1; j++) {
x = this.modPow(x, 2n, n);
if (x === n - 1n) {
continueOuter = true;
break;
}
}
if (!continueOuter) return false;
}
return true;
}
// Modular exponentiation: base^exp mod mod
modPow(base, exp, mod) {
let result = 1n;
base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) {
result = (result * base) % mod;
}
exp = exp / 2n;
base = (base * base) % mod;
}
return result;
}
}
Step 2: RSA Parameter Calculation
class RSAKeyGenerator {
async generateKeyPair() {
console.log('Generating RSA key pair...');
// Step 1: Generate two distinct primes
const p = await this.generatePrime(this.keySize / 2);
const q = await this.generatePrime(this.keySize / 2);
// Ensure p ≠ q
if (p === q) {
return this.generateKeyPair(); // Regenerate
}
// Step 2: Compute n = p × q
const n = p * q;
// Step 3: Compute φ(n) = (p-1) × (q-1)
const phi = (p - 1n) * (q - 1n);
// Step 4: Choose e (public exponent)
const e = BigInt(this.publicExponent);
// Verify gcd(e, φ(n)) = 1
if (this.gcd(e, phi) !== 1n) {
throw new Error('Invalid public exponent');
}
// Step 5: Compute d = e^(-1) mod φ(n)
const d = this.modInverse(e, phi);
// Step 6: Compute CRT parameters for optimization
const dp = d % (p - 1n); // d mod (p-1)
const dq = d % (q - 1n); // d mod (q-1)
const qInv = this.modInverse(q, p); // q^(-1) mod p
return {
publicKey: {
n: n,
e: e,
keySize: this.keySize
},
privateKey: {
n: n,
e: e,
d: d,
p: p,
q: q,
dp: dp,
dq: dq,
qInv: qInv,
keySize: this.keySize
}
};
}
// Extended Euclidean algorithm for modular inverse
modInverse(a, m) {
const [gcd, x, y] = this.extendedGCD(a, m);
if (gcd !== 1n) {
throw new Error('Modular inverse does not exist');
}
return (x % m + m) % m;
}
extendedGCD(a, b) {
if (b === 0n) {
return [a, 1n, 0n];
}
const [gcd, x1, y1] = this.extendedGCD(b, a % b);
const x = y1;
const y = x1 - (a / b) * y1;
return [gcd, x, y];
}
gcd(a, b) {
while (b !== 0n) {
[a, b] = [b, a % b];
}
return a;
}
}
Step 3: Key Export and Serialization
class RSAKeyExporter {
// Export to PEM format
exportPrivateKeyPEM(privateKey, format = 'PKCS1') {
const der = this.privateKeyToDER(privateKey, format);
const base64 = this.arrayBufferToBase64(der);
const header = format === 'PKCS1'
? '-----BEGIN RSA PRIVATE KEY-----'
: '-----BEGIN PRIVATE KEY-----';
const footer = format === 'PKCS1'
? '-----END RSA PRIVATE KEY-----'
: '-----END PRIVATE KEY-----';
return `${header}\n${this.formatBase64(base64)}\n${footer}`;
}
exportPublicKeyPEM(publicKey) {
const der = this.publicKeyToDER(publicKey);
const base64 = this.arrayBufferToBase64(der);
return `-----BEGIN PUBLIC KEY-----\n${this.formatBase64(base64)}\n-----END PUBLIC KEY-----`;
}
// Export to SSH format
exportSSHPublicKey(publicKey) {
const parts = [
this.encodeString('ssh-rsa'),
this.encodeBigInt(publicKey.e),
this.encodeBigInt(publicKey.n)
];
const buffer = this.concatenateBuffers(parts);
const base64 = this.arrayBufferToBase64(buffer);
return `ssh-rsa ${base64} generated-key`;
}
// Export to JWK format
exportJWK(publicKey, privateKey = null) {
const jwk = {
kty: 'RSA',
n: this.base64UrlEncode(this.bigIntToBytes(publicKey.n)),
e: this.base64UrlEncode(this.bigIntToBytes(publicKey.e))
};
if (privateKey) {
jwk.d = this.base64UrlEncode(this.bigIntToBytes(privateKey.d));
jwk.p = this.base64UrlEncode(this.bigIntToBytes(privateKey.p));
jwk.q = this.base64UrlEncode(this.bigIntToBytes(privateKey.q));
jwk.dp = this.base64UrlEncode(this.bigIntToBytes(privateKey.dp));
jwk.dq = this.base64UrlEncode(this.bigIntToBytes(privateKey.dq));
jwk.qi = this.base64UrlEncode(this.bigIntToBytes(privateKey.qInv));
}
return jwk;
}
formatBase64(base64, lineLength = 64) {
const lines = [];
for (let i = 0; i < base64.length; i += lineLength) {
lines.push(base64.substring(i, i + lineLength));
}
return lines.join('\n');
}
}
Encryption and Decryption Implementation
class RSACrypto {
// Encrypt with public key using OAEP padding
async encrypt(message, publicKey) {
// Convert message to bytes
const messageBytes = new TextEncoder().encode(message);
// Apply OAEP padding
const paddedMessage = await this.applyOAEPPadding(
messageBytes,
publicKey.keySize / 8
);
// Convert to BigInt
const m = this.bytesToBigInt(paddedMessage);
// Encrypt: c = m^e mod n
const c = this.modPow(m, publicKey.e, publicKey.n);
// Convert to bytes
return this.bigIntToBytes(c, publicKey.keySize / 8);
}
// Decrypt with private key
async decrypt(ciphertext, privateKey) {
// Convert ciphertext to BigInt
const c = this.bytesToBigInt(ciphertext);
// Decrypt using CRT for optimization
const m = this.decryptCRT(c, privateKey);
// Convert to bytes
const paddedMessage = this.bigIntToBytes(m, privateKey.keySize / 8);
// Remove OAEP padding
const message = await this.removeOAEPPadding(
paddedMessage,
privateKey.keySize / 8
);
// Convert to string
return new TextDecoder().decode(message);
}
// Optimized decryption using Chinese Remainder Theorem
decryptCRT(c, privateKey) {
const { p, q, dp, dq, qInv } = privateKey;
// m1 = c^dp mod p
const m1 = this.modPow(c, dp, p);
// m2 = c^dq mod q
const m2 = this.modPow(c, dq, q);
// h = qInv × (m1 - m2) mod p
let h = (m1 - m2) % p;
if (h < 0n) h += p;
h = (qInv * h) % p;
// m = m2 + h × q
const m = m2 + h * q;
return m;
}
// OAEP padding (Optimal Asymmetric Encryption Padding)
async applyOAEPPadding(message, targetLength) {
const hashLength = 32; // SHA-256
const maxMessageLength = targetLength - 2 * hashLength - 2;
if (message.length > maxMessageLength) {
throw new Error(`Message too long (max ${maxMessageLength} bytes)`);
}
// Generate random seed
const seed = crypto.getRandomValues(new Uint8Array(hashLength));
// Create data block
const dataBlock = new Uint8Array(targetLength - hashLength - 1);
const labelHash = await crypto.subtle.digest('SHA-256', new Uint8Array(0));
dataBlock.set(new Uint8Array(labelHash), 0);
dataBlock[hashLength + maxMessageLength - message.length] = 0x01;
dataBlock.set(message, hashLength + maxMessageLength - message.length + 1);
// MGF1 masking
const dbMask = await this.mgf1(seed, dataBlock.length);
const maskedDB = this.xor(dataBlock, dbMask);
const seedMask = await this.mgf1(maskedDB, hashLength);
const maskedSeed = this.xor(seed, seedMask);
// Construct padded message
const padded = new Uint8Array(targetLength);
padded[0] = 0x00;
padded.set(maskedSeed, 1);
padded.set(maskedDB, hashLength + 1);
return padded;
}
async removeOAEPPadding(paddedMessage, expectedLength) {
const hashLength = 32; // SHA-256
if (paddedMessage[0] !== 0x00) {
throw new Error('Decryption error: Invalid padding');
}
const maskedSeed = paddedMessage.slice(1, hashLength + 1);
const maskedDB = paddedMessage.slice(hashLength + 1);
// Reverse MGF1 masking
const seedMask = await this.mgf1(maskedDB, hashLength);
const seed = this.xor(maskedSeed, seedMask);
const dbMask = await this.mgf1(seed, maskedDB.length);
const db = this.xor(maskedDB, dbMask);
// Find 0x01 separator
let separatorIndex = -1;
for (let i = hashLength; i < db.length; i++) {
if (db[i] === 0x01) {
separatorIndex = i;
break;
} else if (db[i] !== 0x00) {
throw new Error('Decryption error: Invalid padding format');
}
}
if (separatorIndex === -1) {
throw new Error('Decryption error: Separator not found');
}
return db.slice(separatorIndex + 1);
}
// MGF1 (Mask Generation Function)
async mgf1(seed, length) {
const mask = new Uint8Array(length);
const hashLength = 32;
let counter = 0;
let offset = 0;
while (offset < length) {
const counterBytes = new Uint8Array(4);
new DataView(counterBytes.buffer).setUint32(0, counter, false);
const input = new Uint8Array(seed.length + 4);
input.set(seed);
input.set(counterBytes, seed.length);
const hash = await crypto.subtle.digest('SHA-256', input);
const hashArray = new Uint8Array(hash);
const copyLength = Math.min(hashLength, length - offset);
mask.set(hashArray.slice(0, copyLength), offset);
offset += copyLength;
counter++;
}
return mask;
}
xor(a, b) {
const result = new Uint8Array(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = a[i] ^ b[i];
}
return result;
}
}
Digital Signature Implementation
class RSASignature {
// Sign message with private key
async sign(message, privateKey) {
// Hash message
const messageBytes = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', messageBytes);
const hash = new Uint8Array(hashBuffer);
// Apply PKCS#1 v1.5 signature padding
const padded = this.applySignaturePadding(hash, privateKey.keySize / 8);
// Convert to BigInt
const m = this.bytesToBigInt(padded);
// Sign: s = m^d mod n
const s = this.modPow(m, privateKey.d, privateKey.n);
// Convert to bytes and encode
const signature = this.bigIntToBytes(s, privateKey.keySize / 8);
return this.arrayBufferToBase64(signature);
}
// Verify signature with public key
async verify(message, signature, publicKey) {
try {
// Decode signature
const signatureBytes = this.base64ToArrayBuffer(signature);
const s = this.bytesToBigInt(signatureBytes);
// Verify: m = s^e mod n
const m = this.modPow(s, publicKey.e, publicKey.n);
const decrypted = this.bigIntToBytes(m, publicKey.keySize / 8);
// Extract hash from padding
const extractedHash = this.removeSignaturePadding(
decrypted,
publicKey.keySize / 8
);
// Hash original message
const messageBytes = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', messageBytes);
const expectedHash = new Uint8Array(hashBuffer);
// Compare hashes
return this.constantTimeCompare(extractedHash, expectedHash);
} catch (error) {
return false;
}
}
applySignaturePadding(hash, targetLength) {
// PKCS#1 v1.5 signature padding
// 0x00 || 0x01 || PS || 0x00 || DigestInfo || Hash
const digestInfo = new Uint8Array([
0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20
]);
const paddingLength = targetLength - hash.length - digestInfo.length - 3;
if (paddingLength < 8) {
throw new Error('Key size too small for signature');
}
const padded = new Uint8Array(targetLength);
padded[0] = 0x00;
padded[1] = 0x01;
padded.fill(0xff, 2, 2 + paddingLength);
padded[2 + paddingLength] = 0x00;
padded.set(digestInfo, 3 + paddingLength);
padded.set(hash, 3 + paddingLength + digestInfo.length);
return padded;
}
removeSignaturePadding(paddedData, expectedLength) {
if (paddedData[0] !== 0x00 || paddedData[1] !== 0x01) {
throw new Error('Invalid signature padding');
}
let separatorIndex = 2;
while (separatorIndex < paddedData.length && paddedData[separatorIndex] === 0xff) {
separatorIndex++;
}
if (paddedData[separatorIndex] !== 0x00) {
throw new Error('Invalid signature padding format');
}
// Skip DigestInfo and return hash
const digestInfoLength = 19; // SHA-256 DigestInfo
return paddedData.slice(separatorIndex + 1 + digestInfoLength);
}
constantTimeCompare(a, b) {
if (a.length !== b.length) {
return false;
}
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a[i] ^ b[i];
}
return diff === 0;
}
}
Comparison with Symmetric Encryption
When to Use RSA vs. AES
RSA (Asymmetric) - Best For:
- Digital signatures and authentication
- Key exchange in hybrid encryption
- Identity verification
- Certificate-based systems
- Decentralized trust models
AES (Symmetric) - Best For:
- Bulk data encryption
- File and database encryption
- Fast real-time encryption
- Large data volumes
- Known, trusted parties
Hybrid Encryption (Recommended):
class HybridEncryption {
async encrypt(data, recipientRSAPublicKey) {
// 1. Generate random AES key
const aesKey = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
// 2. Encrypt data with AES (fast)
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
aesKey,
new TextEncoder().encode(data)
);
// 3. Export AES key
const exportedKey = await crypto.subtle.exportKey('raw', aesKey);
// 4. Encrypt AES key with RSA (small, secure)
const encryptedKey = await this.rsaEncrypt(
new Uint8Array(exportedKey),
recipientRSAPublicKey
);
return {
encryptedData: this.arrayBufferToBase64(encryptedData),
encryptedKey: this.arrayBufferToBase64(encryptedKey),
iv: this.arrayBufferToBase64(iv)
};
}
async decrypt(encryptedPackage, recipientRSAPrivateKey) {
// 1. Decrypt AES key with RSA
const encryptedKey = this.base64ToArrayBuffer(encryptedPackage.encryptedKey);
const aesKeyBytes = await this.rsaDecrypt(encryptedKey, recipientRSAPrivateKey);
// 2. Import AES key
const aesKey = await crypto.subtle.importKey(
'raw',
aesKeyBytes,
{ name: 'AES-GCM' },
false,
['decrypt']
);
// 3. Decrypt data with AES
const encryptedData = this.base64ToArrayBuffer(encryptedPackage.encryptedData);
const iv = this.base64ToArrayBuffer(encryptedPackage.iv);
const decryptedData = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
aesKey,
encryptedData
);
return new TextDecoder().decode(decryptedData);
}
}
Use our AES Encryptor/Decryptor for the symmetric encryption component.
Performance Comparison
| Operation | RSA-2048 | AES-256 | Speedup |
|---|---|---|---|
| Key Generation | ~100ms | <1ms | 100x |
| Encryption (1KB) | ~5ms | <0.1ms | 50x |
| Decryption (1KB) | ~15ms | <0.1ms | 150x |
| Signature | ~15ms | N/A | - |
| Verification | ~2ms | N/A | - |
Conclusion: Use RSA for key exchange and signatures, AES for data encryption.
Best Practices and Security Guidelines
Key Generation Best Practices
Prime Selection:
// ✅ Good: Use strong random primes
const p = await generateStrongPrime(keySize / 2);
const q = await generateStrongPrime(keySize / 2);
// Ensure sufficient distance
if (Math.abs(p - q) < 2^(keySize/2 - 100)) {
// Regenerate, primes too close
}
// ✅ Check that p and q are not special-form primes
// (e.g., Mersenne primes, Sophie Germain primes without sufficient randomness)
Public Exponent Selection:
// ✅ Recommended: e = 65537 (F4)
const e = 65537;
// ⚠️ Acceptable: e = 3 (faster but less margin of security)
// const e = 3;
// ❌ Never: e = 1 (completely insecure)
Private Key Protection:
// ✅ Encrypt private key for storage
async function encryptPrivateKey(privateKey, passphrase) {
const salt = crypto.getRandomValues(new Uint8Array(16));
// Derive encryption key from passphrase
const key = await deriveKeyFromPassword(passphrase, salt, 100000);
// Encrypt private key
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
privateKey
);
return { encrypted, salt, iv };
}
Key Management
Key Rotation Schedule:
class KeyRotationManager {
constructor(rotationPolicy) {
this.rotationPolicy = rotationPolicy; // e.g., { maxAge: 365 * 24 * 60 * 60 * 1000 }
}
shouldRotate(keyMetadata) {
const keyAge = Date.now() - keyMetadata.createdAt;
// Rotate based on age
if (keyAge > this.rotationPolicy.maxAge) {
return true;
}
// Rotate based on usage count
if (keyMetadata.usageCount > this.rotationPolicy.maxUsage) {
return true;
}
// Rotate if compromise suspected
if (keyMetadata.compromiseSuspected) {
return true;
}
return false;
}
async rotateKey(oldKeyId, purpose) {
// Generate new key pair
const newKeyPair = await generateRSAKeyPair(2048);
// Transition period: accept both keys
await this.enableDualKeyAcceptance(oldKeyId, newKeyPair.id);
// Update all systems with new public key
await this.distributeNewPublicKey(newKeyPair.publicKey);
// Wait for propagation
await this.wait(this.rotationPolicy.transitionPeriod);
// Revoke old key
await this.revokeKey(oldKeyId);
// Securely delete old private key
await this.secureDelete(oldKeyId);
return newKeyPair;
}
}
Key Storage Options:
Security Hierarchy (Best to Worst):
1. ✅✅✅ Hardware Security Module (HSM)
- FIPS 140-2 Level 3/4
- Tamper-evident/resistant
- Physical key protection
- Cost: $$$$$
2. ✅✅ Trusted Platform Module (TPM)
- Hardware-backed key storage
- Available on modern laptops
- OS integration
- Cost: $ (built-in)
3. ✅ Key Management Service (KMS)
- AWS KMS, Azure Key Vault, Google Cloud KMS
- Centralized management
- Audit logging
- Cost: $$
4. ✅ Password-Protected Files
- Encrypted with strong passphrase
- Stored on encrypted disk
- Regular backups
- Cost: Free
5. ⚠️ Environment Variables
- Better than hardcoding
- Still in memory
- Limited access control
- Cost: Free
6. ❌ Hardcoded in Source Code
- Exposed in version control
- No access control
- High compromise risk
- Never do this!
Security Audit Checklist
Before deploying RSA in production:
- Key size ≥ 2048 bits (preferably 3072 or 4096)
- Using standard public exponent (65537)
- Cryptographically secure random number generation
- OAEP padding for encryption (not PKCS#1 v1.5)
- PSS padding for signatures (or PKCS#1 v1.5 with caution)
- Private keys encrypted at rest
- Key rotation policy implemented
- Key usage logged and monitored
- Backup and recovery procedures tested
- Constant-time implementations (avoid timing attacks)
- Input validation on all cryptographic operations
- Error handling doesn’t leak sensitive information
- Certificate validation for PKI systems
- Regular security audits and penetration testing
Case Study: Building a Secure Document Signing System
Challenge
A legal technology company needed to implement a digital signature system for legally binding contracts, requiring:
- Legal compliance: eIDAS (EU), ESIGN Act (US) requirements
- Non-repudiation: Signers cannot deny signing documents
- Integrity verification: Detect any document tampering
- Long-term validity: Signatures remain valid for decades
- Audit trail: Complete signing history with timestamps
- Performance: Handle 10,000+ signatures per day
Solution Architecture
Document Signing Workflow:
1. User Registration
├── Generate 4096-bit RSA key pair
├── Store private key encrypted with user password
├── Register public key with Certificate Authority
└── Issue identity certificate
2. Document Preparation
├── User uploads document (PDF, DOCX, etc.)
├── System generates cryptographic hash (SHA-256)
├── Create signature metadata (signer, timestamp, purpose)
└── Prepare signature request
3. Signature Generation
├── User authenticates (MFA)
├── Decrypt private key with password
├── Sign document hash with RSA-PSS
├── Include timestamp from trusted TSA
└── Create signature bundle
4. Signature Verification
├── Extract document and signature
├── Retrieve signer's public key certificate
├── Verify certificate chain to root CA
├── Verify document hash matches signature
├── Check timestamp authenticity
└── Generate verification report
Implementation:
class DocumentSigningSystem {
async signDocument(document, signerPrivateKey, signerCertificate) {
// 1. Hash document
const documentHash = await crypto.subtle.digest(
'SHA-256',
document
);
// 2. Get trusted timestamp
const timestamp = await this.getTrustedTimestamp(documentHash);
// 3. Create signature metadata
const metadata = {
signer: signerCertificate.subject,
timestamp: timestamp.time,
algorithm: 'RSA-PSS-SHA256',
documentHash: this.arrayBufferToBase64(documentHash)
};
// 4. Create data to sign (metadata + hash)
const dataToSign = new TextEncoder().encode(
JSON.stringify(metadata) + this.arrayBufferToBase64(documentHash)
);
// 5. Generate RSA-PSS signature
const signature = await this.rsaSign(dataToSign, signerPrivateKey);
// 6. Create signature bundle
return {
document: this.arrayBufferToBase64(document),
signature: this.arrayBufferToBase64(signature),
metadata: metadata,
timestamp: timestamp,
certificate: signerCertificate
};
}
async verifySignature(signedDocument) {
const {
document,
signature,
metadata,
timestamp,
certificate
} = signedDocument;
// 1. Verify certificate chain
const certValid = await this.verifyCertificateChain(certificate);
if (!certValid) {
return { valid: false, reason: 'Invalid certificate' };
}
// 2. Check certificate not revoked
const revoked = await this.checkCertificateRevocation(certificate);
if (revoked) {
return { valid: false, reason: 'Certificate revoked' };
}
// 3. Verify timestamp
const timestampValid = await this.verifyTimestamp(timestamp);
if (!timestampValid) {
return { valid: false, reason: 'Invalid timestamp' };
}
// 4. Recompute document hash
const documentBytes = this.base64ToArrayBuffer(document);
const computedHash = await crypto.subtle.digest('SHA-256', documentBytes);
// 5. Verify hash matches metadata
if (this.arrayBufferToBase64(computedHash) !== metadata.documentHash) {
return { valid: false, reason: 'Document modified' };
}
// 6. Reconstruct signed data
const dataToVerify = new TextEncoder().encode(
JSON.stringify(metadata) + metadata.documentHash
);
// 7. Verify RSA signature
const signatureBytes = this.base64ToArrayBuffer(signature);
const signatureValid = await this.rsaVerify(
dataToVerify,
signatureBytes,
certificate.publicKey
);
if (!signatureValid) {
return { valid: false, reason: 'Invalid signature' };
}
return {
valid: true,
signer: metadata.signer,
signedAt: metadata.timestamp,
algorithm: metadata.algorithm,
certificate: certificate
};
}
}
Results
After 18 months of operation:
- ✅ 250,000+ documents signed with zero repudiation incidents
- ✅ 100% legal compliance across EU and US jurisdictions
- ✅ <500ms average signing time (excellent UX)
- ✅ <100ms average verification time
- ✅ Zero false positives in signature verification
- ✅ Complete audit trail for regulatory compliance
- ✅ Long-term archive with 30-year signature validity
Key Lessons
- Use 4096-bit keys for long-term signatures (30+ year validity)
- Include trusted timestamps to prove signing time
- Implement certificate revocation checking (CRL or OCSP)
- Hash-then-sign approach separates document from signature
- Comprehensive logging essential for legal compliance
- User experience matters - make signing seamless but secure
Call to Action
Implement RSA Security Today
For Developers:
- Generate production-grade RSA keys with our RSA Key Generator
- Implement JWT authentication with RSA signatures
- Add SSH public key authentication to your servers
- Replace shared secret authentication with PKI
- Implement code signing for software distribution
For Security Teams:
- Audit existing RSA implementations
- Establish key rotation policies
- Deploy centralized key management systems
- Implement certificate lifecycle management
- Conduct regular cryptographic assessments
For Organizations:
- Transition to passwordless authentication
- Implement digital signature workflows
- Deploy Public Key Infrastructure (PKI)
- Enable encrypted communications
- Ensure regulatory compliance (eIDAS, ESIGN Act)
Enhance Your Cryptography Toolkit
Explore complementary tools:
- AES Encryptor/Decryptor: Symmetric encryption for hybrid systems
- Hash Generator: Create message digests and fingerprints
- Password Strength Checker: Validate private key passphrases
- UUID Generator: Generate unique identifiers for key management
Continue Learning
Advanced Topics:
- Post-Quantum Cryptography (PQC)
- Zero-Knowledge Proofs
- Threshold Cryptography
- Homomorphic Encryption
- Blockchain and Cryptocurrency
Stay Updated:
- Follow NIST cryptographic standards
- Monitor quantum computing advances
- Subscribe to security advisories
- Participate in cryptography conferences
External References
-
PKCS #1 v2.2: RSA Cryptography Standard. RSA Laboratories. https://www.rfc-editor.org/rfc/rfc8017
-
NIST FIPS 186-5: Digital Signature Standard (DSS). National Institute of Standards and Technology. https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf
-
“A Method for Obtaining Digital Signatures and Public-Key Cryptosystems” by Rivest, Shamir, and Adleman. Communications of the ACM, 1978. The original RSA paper.
-
OWASP Cryptographic Storage Cheat Sheet: Best practices for cryptographic key management. https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html
Last updated: November 3, 2025 | Article ID: rsa-key-generator-guide | Gray-wolf Tools Security Division