
Digital Signature: The Internet's Seal
Digitized paper signature. Verify with public key, sign with private key. Unforgeable, non-repudiation. Core tech of blockchain and HTTPS.

Digitized paper signature. Verify with public key, sign with private key. Unforgeable, non-repudiation. Core tech of blockchain and HTTPS.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Kept seeing "digital signature" while studying blockchain. "Bitcoin transactions are signed with a private key" - but how do you sign something digitally? I understood stamping paper documents, but how do you stamp a file?
It got even more confusing when studying HTTPS and encountering "CA signs the server certificate." A signature is something you write by hand, so how does a computer sign something?
Digital signatures are already everywhere in real life. I've seen apps where you sign contracts with your finger, and heard phrases like "sign with your certificate," but I didn't know exactly how it worked. I just understood it as "a digital stamp."
Then I saw "package signing" when publishing npm packages, and learned that Docker images can also be signed. Digital signatures are everywhere in the software world. I realized I couldn't understand security without properly understanding this, so I dug in.
I understood public key encryption. When Bob sends a secret message to Alice, he encrypts it with Alice's public key, and only Alice's private key can decrypt it. That was intuitive.
But digital signatures were the complete opposite. Encrypting with a private key and decrypting with a public key - what does that even mean? Anyone can have the public key, so if it can be decrypted with that, it's not a secret, right? Then what's the point of encryption?
And the concept of "signing the hash" was confusing. Not signing the document itself, but signing the hash? A hash is like a fingerprint, so what does it mean to stamp a fingerprint?
The most confusing part was this: "How can you trust it if you only verify with the public key?" Anyone can have a public key. So what if Eve creates a fake public key and says "this is Alice's public key"? I later learned that CA (Certificate Authority) solves this problem.
The decisive moment when I understood digital signatures was this: Encryption's purpose isn't just "keeping secrets."
Like medieval nobles sealing letters with wax seals, digital signatures work the same way. A wax seal doesn't hide the letter's contents. Anyone can break the seal and read it. But if the seal is intact, it proves the letter wasn't tampered with and really came from that noble.
I understood the difference between public key encryption and digital signatures like this:
graph LR
A[Public Key Encryption] --> B[Purpose: Secrecy]
A --> C[Lock with public key]
A --> D[Only unlocks with private key]
E[Digital Signature] --> F[Purpose: Prove Identity]
E --> G[Lock with private key]
E --> H[Anyone unlocks with public key]
Public Key Encryption: Lock it so others can't open it (secrecy) Digital Signature: Prove that I locked it (prove identity)
Once I understood this difference, "encrypting with a private key" made sense. It's not about creating a secret - it's proof that "only I can create this lock."
Digital signatures have two main stages: creating the signature and verifying it. I understood these as "stamping a wax seal" and "checking the wax seal."
Say Alice signs a document saying "Send 100 to Bob."
sequenceDiagram
participant A as Alice
participant D as Document
participant H as Hash Function
participant S as Signature
A->>D: "Send 100 to Bob"
D->>H: Pass document
H->>H: SHA256 hashing
H->>S: Hash value: a3c7f9...
A->>S: Encrypt with private key
S->>S: Signature created
S->>D: Document + Signature
The key here is not encrypting the entire document. If the document is 10MB, RSA encryption would be extremely slow, so we hash it first. A SHA256 hash is always 256 bits (32 bytes) no matter how large the document.
Then we encrypt this hash with the private key. This becomes the "signature." Only Alice's private key can create this signature. Nobody can fake it. This is like a noble stamping wax with their coat of arms seal.
Bob receives the document and signature. Now he needs to verify it really came from Alice.
sequenceDiagram
participant B as Bob
participant D as Document
participant S as Signature
participant P as Alice Public Key
participant V as Verify
B->>D: Received document
B->>S: Received signature
D->>V: SHA256 hash → HashB
S->>P: Decrypt with public key → HashA
V->>V: HashA == HashB?
V->>B: Verification result
Bob does two things:
If HashA and HashB match, two things are proven simultaneously:
This was the essence. Digital signatures prove both "who" sent it and that it "wasn't tampered with" at the same time.
Just reading concepts makes you think you understand, but you forget quickly. Actually coding it made it click.
const crypto = require('crypto');
// 1. Generate key pair (Alice's private and public keys)
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048, // 2048-bit RSA
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
console.log("Alice's public key:\n", publicKey);
console.log("Alice's private key:\n", privateKey);
// 2. Alice signs document
const message = "Send 100 to Bob";
const sign = crypto.createSign('SHA256');
sign.update(message);
sign.end();
const signature = sign.sign(privateKey, 'hex');
console.log("\nSignature:", signature);
console.log("Signature length:", signature.length, "characters");
// 3. Bob verifies signature
const verify = crypto.createVerify('SHA256');
verify.update(message);
verify.end();
const isValid = verify.verify(publicKey, signature, 'hex');
console.log("\nVerification result:", isValid); // true
// 4. What if attacker Eve tampers with document?
const verifyTampered = crypto.createVerify('SHA256');
verifyTampered.update("Send 1000 to Bob"); // 100 → 1000 tampered!
verifyTampered.end();
const isValidTampered = verifyTampered.verify(publicKey, signature, 'hex');
console.log("Tampered document verification:", isValidTampered); // false
// 5. What if Eve signs with different private key?
const { publicKey: evePublic, privateKey: evePrivate } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048
});
const eveSign = crypto.createSign('SHA256');
eveSign.update(message); // Same document
eveSign.end();
const eveSignature = eveSign.sign(evePrivate, 'hex');
const verifyEve = crypto.createVerify('SHA256');
verifyEve.update(message);
verifyEve.end();
const isValidEve = verifyEve.verify(publicKey, eveSignature, 'hex');
console.log("Eve's signature verified with Alice's public key:", isValidEve); // false
Running this code, I realized: verification only passes when both document and key match perfectly. Change the document even slightly and the hash completely changes - verification fails. Sign with someone else's private key and the public key doesn't match - verification fails. Two security mechanisms working simultaneously.
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
# Generate keys
key = RSA.generate(2048)
private_key = key
public_key = key.publickey()
# Create signature
message = b"Send 100 to Bob"
hash_obj = SHA256.new(message)
signature = pkcs1_15.new(private_key).sign(hash_obj)
print(f"Signature: {signature.hex()}")
# Verify signature
try:
hash_verify = SHA256.new(message)
pkcs1_15.new(public_key).verify(hash_verify, signature)
print("✅ Verification success! Alice signed and document wasn't tampered.")
except ValueError:
print("❌ Verification failed! Forged or tampered.")
# Attempt tampering
tampered_message = b"Send 1000 to Bob"
try:
hash_tampered = SHA256.new(tampered_message)
pkcs1_15.new(public_key).verify(hash_tampered, signature)
print("✅ Tampered document verified")
except ValueError:
print("❌ Tampered document verification failed (expected)")
The Python code is more explicit. pkcs1_15 is one of the RSA signature standards, using exception handling to determine verification success/failure. The try-catch structure shows the binary nature of signature verification - it either succeeds or fails.
Initially I didn't understand "why not just encrypt the document directly instead of creating a hash first?" It seemed unnecessarily complicated to add an extra step.
But testing with large files made the reason clear.
const crypto = require('crypto');
// Create 10MB file
const largeFile = Buffer.alloc(10 * 1024 * 1024, 'a');
// Method 1: Encrypt entire file (slow)
console.time('Direct signing');
// RSA has data size limits - can't directly sign large files!
// Theoretically extremely slow too
console.timeEnd('Direct signing');
// Method 2: Hash then sign (fast)
console.time('Hash then sign');
const hash = crypto.createHash('SHA256').update(largeFile).digest();
console.log('Hash size:', hash.length, 'bytes'); // 32 bytes
// Now only need to sign 32 bytes
console.timeEnd('Hash then sign');
RSA encryption has size limitations. 2048-bit RSA can only encrypt up to 245 bytes. Anything larger needs to be split into multiple blocks and encrypted separately, which is extremely slow.
In contrast, SHA256 hash always produces 32-byte output whether the input is 1 byte or 10GB. We only need to RSA-encrypt 32 bytes, so it's fast.
Another crucial property of hashing is the "avalanche effect." Changing even 1 bit of input completely changes the output.
import hashlib
def hash_string(s):
return hashlib.sha256(s.encode()).hexdigest()
msg1 = "Send 100 to Bob"
msg2 = "Send 101 to Bob" # 0 → 1, one character changed
msg3 = "Send 100 to Bob " # Added space at end
print(f"Original: {hash_string(msg1)}")
print(f"1 char: {hash_string(msg2)}")
print(f"Space: {hash_string(msg3)}")
# Output:
# Original: a3c7f912b54d8e9c7f8a6b3d2e1f4c5a...
# 1 char: df82b1c3a9f7e6d5c4b3a2918f7e6d5c...
# Space: 8f2e9d4c5b6a7f8e9d0c1b2a3f4e5d6c...
Completely different hashes. This matters because it's virtually impossible for an attacker to create a "fake document with the same hash as the original." This is called "collision resistance."
RSA isn't the only digital signature algorithm. Nowadays ECDSA (Elliptic Curve Digital Signature Algorithm) is widely used. I first encountered ECDSA while studying Bitcoin.
const crypto = require('crypto');
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048 // 2048 bits = 256 bytes key
});
// Public key size
const publicKeyPEM = publicKey.export({ type: 'spki', format: 'pem' });
console.log('RSA public key size:', publicKeyPEM.length, 'bytes');
RSA is an old, well-tested algorithm. Easy to understand, many implementations, good compatibility. But key sizes are large. 2048-bit RSA is standard, which is 256 bytes.
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'secp256k1' // Curve Bitcoin uses
});
const publicKeyPEM = publicKey.export({ type: 'spki', format: 'pem' });
console.log('ECDSA public key size:', publicKeyPEM.length, 'bytes');
ECDSA uses elliptic curve cryptography. 256-bit ECDSA provides similar security to 3072-bit RSA. Smaller keys, faster signing, suitable for mobile/IoT. Bitcoin and Ethereum use ECDSA.
I understood it like this:
Both are mathematically secure, but chosen based on use case. HTTPS mainly uses RSA, blockchain uses ECDSA.
Whenever I encounter JWT (JSON Web Token) in web development, digital signatures appear.
const jwt = require('jsonwebtoken');
const payload = {
userId: 123,
username: 'alice',
role: 'admin'
};
const privateKey = `-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----`;
// Create JWT (RS256 = RSA + SHA256)
const token = jwt.sign(payload, privateKey, {
algorithm: 'RS256',
expiresIn: '1h'
});
console.log('JWT:', token);
// Verify JWT
const publicKey = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`;
try {
const decoded = jwt.verify(token, publicKey);
console.log('Verification success:', decoded);
} catch (err) {
console.log('Verification failed:', err.message);
}
JWT has three parts:
Thanks to the signature, clients can't tamper with the payload. If they do, signature verification fails. This is digital signature's "integrity guarantee" feature.
When publishing npm packages, you can sign them too.
# Sign npm package
npm publish --provenance
# Verify package
npm audit signatures
npm 7+ supports package signing. When the publisher signs with their private key, users can verify with the public key that "this package really came from the official publisher." Even if a malicious mirror site tampers with the package, signature verification fails, keeping it safe.
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
# Push image (automatically signed)
docker push myrepo/myimage:latest
# Pull image (automatically verified)
docker pull myrepo/myimage:latest
Docker Content Trust uses a signing system called Notary. It automatically signs when pushing images and verifies when pulling. This prevents receiving malicious images through man-in-the-middle attacks.
# Generate GPG key
gpg --gen-key
# Configure Git with GPG key
git config --global user.signingkey YOUR_KEY_ID
# Sign commit
git commit -S -m "Add authentication feature"
# Verify signature
git log --show-signature
# Output:
# commit abc123 (HEAD -> main)
# gpg: Signature made Fri Apr 23 10:30:00 2025 KST
# gpg: Good signature from "Alice <alice@example.com>"
# Author: Alice <alice@example.com>
# Date: Fri Apr 23 10:30:00 2025
#
# Add authentication feature
You've probably seen the green "Verified" badge on GitHub. That's GPG signing. It proves the commit really came from that developer. Even if someone hacks Alice's account and makes commits, without Alice's GPG private key, the "Verified" badge won't appear.
The most confusing thing when understanding HTTPS was the "certificate chain."
Root CA (trusted authority, built into browser)
↓ signs
Intermediate CA
↓ signs
google.com server certificate
When a browser connects to google.com:
This is the "Chain of Trust." I don't know the Root CA personally, but the browser trusts it, so I trust it. The Root CA trusts the Intermediate CA, so I trust it. The Intermediate CA trusts google.com, so I trust it.
Without digital signatures, this trust chain is impossible. Each step needs a signature to prove "the previous step vouches for the next step."
The biggest problem I encountered studying digital signatures was this: How do you trust the public key itself?
Alice sends Bob "this is my public key." But what if Eve intercepts it and sends her own public key instead? Bob mistakes Eve's public key for Alice's and trusts Eve's signature as Alice's. This is called a "Man-in-the-Middle Attack."
sequenceDiagram
participant A as Alice
participant E as Eve (attacker)
participant B as Bob
A->>E: My public key: PubKey_Alice
E->>E: Intercept!
E->>B: Lie it's Alice's key: PubKey_Eve
B->>B: Mistake Eve's key for Alice's
E->>B: Document signed by Eve
B->>B: "Alice signed this!" (fooled)
CA solves this problem.
sequenceDiagram
participant A as Alice
participant CA as Certificate Authority
participant B as Bob
A->>CA: Proof of identity + public key
CA->>CA: Verify Alice's identity (passport, business license, etc.)
CA->>A: Issue certificate (Alice public key + CA signature)
A->>B: Send certificate
B->>B: Verify signature with CA public key
B->>B: "CA vouched for it, so it's really Alice's public key!"
The CA's essence is being a "trusted third party." After verifying Alice's identity, the CA creates a certificate by signing Alice's public key with the CA's signature. Now Bob only needs to trust the CA. The CA signed saying "this is Alice's public key," after all.
# Install Certbot (Let's Encrypt client)
sudo apt install certbot
# Get certificate (issued after domain ownership verification)
sudo certbot certonly --standalone -d example.com
# Check issued certificate
sudo cat /etc/letsencrypt/live/example.com/fullchain.pem
Let's Encrypt is a free CA. It automatically issues certificates after verifying domain ownership. This certificate contains:
When a browser connects to example.com, it receives the certificate and verifies the signature with Let's Encrypt's public key. Verification succeeds → "This public key really belongs to example.com" → HTTPS connection.
After understanding theory, you need to attempt attacks to truly feel why it's secure.
const crypto = require('crypto');
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048
});
// Alice signs original document
const originalDoc = "Send 100 to Bob";
const sign1 = crypto.createSign('SHA256');
sign1.update(originalDoc);
const signature = sign1.sign(privateKey, 'hex');
// Eve copies signature to malicious document
const maliciousDoc = "Send 1000 to Eve";
const verify = crypto.createVerify('SHA256');
verify.update(maliciousDoc);
const isValid = verify.verify(publicKey, signature, 'hex');
console.log("Tampered document with copied signature:", isValid); // false
Why it fails: The signature is the encrypted hash of the document. Change the document and the hash changes, so the decrypted hash doesn't match the new document's hash.
// Eve tries to forge signature without private key
const fakeSignature = "304502..." // Random hex
const verify = crypto.createVerify('SHA256');
verify.update("Send 1000 to Eve");
const isValid = verify.verify(publicKey, fakeSignature, 'hex');
console.log("Forged signature verification:", isValid); // false (error)
Why it fails: RSA signatures are mathematically impossible to generate without the private key. To compute the private key from the public key requires factoring large primes - for 2048-bit RSA, this would take millions of years with current technology.
import hashlib
# Eve tries to create malicious document with same hash as original
original = "Send 100 to Bob"
original_hash = hashlib.sha256(original.encode()).hexdigest()
# Attempt brute force to find same hash
attempts = 0
found = False
# This is actually virtually impossible
# SHA256 has 2^256 possibilities - can't find even with universe's age of attempts
Why it fails: Finding SHA256 collisions is virtually impossible. 2^256 ≈ 10^77 possibilities. The universe has about 10^80 atoms.
Running these three attack scenarios myself, I really felt "yes, it's truly secure."
After studying digital signatures, I summarized it like this:
Digital Signature = Digital Version of Medieval Wax SealWhat wax seals do:
What digital signatures do:
Core principles:
Most important concept I internalized: Encryption's purpose isn't just "keeping secrets." "Proving who created the lock" is also a use of encryption. This was the essence of digital signatures.
Blockchain, HTTPS, npm, Docker, Git... I encounter digital signatures everywhere in the software world. Now when I see "digital signature," I immediately understand it as "a wax seal that locks with private key and opens with public key."