Junior 6 min · March 24, 2026

Digital Signatures — JSON Whitespace Broke Our Webhooks

A $180k webhook failed because json.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Digital signatures provide authentication, integrity, and non-repudiation in one cryptographic operation.
  • Hash the message first (SHA-256), then sign the hash — protects against forgery and handles arbitrary message sizes.
  • Ed25519 is the modern default: 64-byte signatures, deterministic, side-channel resistant, and faster than ECDSA.
  • RSA-PSS is preferred over PKCS#1 v1.5 — the latter is deterministic and vulnerable to Bleichenbacher-style attacks.
  • ECDSA requires a secure random nonce k; reused k exposes the private key (Sony PS3 failure).
  • Signature format mismatches (DER vs raw vs JOSE) are the most common production bug — always agree on the wire format.
Plain-English First

A digital signature is the mathematical equivalent of a handwritten signature but unforgeable. Sign a document with your private key — anyone can verify with your public key that it came from you and was not modified. Unlike a handwritten signature that can be photocopied onto any document, a digital signature cryptographically binds the signature to the exact content — changing a single bit invalidates it.

Think of it like a wax seal on a medieval letter. The seal proves the sender's identity (only they have the signet ring). The seal proves integrity (breaking the seal to modify the letter destroys the seal). And the seal proves non-repudiation (the sender can't claim someone else sealed it — only their ring could produce that pattern). Digital signatures do all three, mathematically, with guarantees that no physical seal can match.

I once spent three days debugging a production issue where a payment gateway rejected every webhook from our service. The error was 'invalid signature.' The code looked correct — same algorithm, same key, same message. The actual problem: our service was signing the raw JSON string, but the gateway was verifying the signature against the minified JSON (no whitespace). One space character difference — and every signature was invalid. The gateway didn't reject our requests with a helpful error. It silently dropped them. $180k in payments queued for three days before anyone noticed.

Digital signatures provide three guarantees simultaneously: authentication (it came from the claimed sender), integrity (it was not modified), and non-repudiation (the sender cannot deny sending it). These three properties together are what make digital signatures legally binding in most jurisdictions under e-signature laws (ESIGN in the US, eIDAS in the EU).

Every HTTPS certificate is signed by a CA. Every Git commit can be signed. Every software package distributed through pip, apt, or npm is signed. Every code signing certificate for macOS/Windows applications uses digital signatures. Every JWT with RS256 or ES256 is a digital signature. The choice of signature scheme — RSA-PSS, ECDSA, Ed25519 — matters for performance, security, and implementation risk.

This guide covers every major signature scheme from first principles, with working code in Python and Java, the real-world applications that make signatures matter, the attacks that break implementations (not algorithms), and the post-quantum alternatives that will replace current schemes within the next decade.

How Digital Signatures Work: Hash-Then-Sign

All digital signature schemes follow the same three-step pattern: hash the message, sign the hash, verify the hash. The hashing step is not optional — it's a critical security requirement.

Step 1: Hash — The message (which can be any size) is passed through a cryptographic hash function (SHA-256, SHA-384, SHA-512) to produce a fixed-size digest (256, 384, or 512 bits). This digest is a fingerprint of the message — change one bit and the hash changes completely (avalanche effect).

Step 2: Sign — The hash is signed with the private key using the chosen algorithm (RSA, ECDSA, Ed25519). The result is the signature — a fixed-size byte string that can only be produced by the holder of the private key.

Step 3: Verify — The verifier hashes the received message with the same hash function, then uses the public key to verify that the signature matches the hash. If the message was modified (even one bit), the hashes don't match and verification fails.

Why hash first? Three reasons: (1) RSA and ECDSA can only sign messages smaller than the key size — hashing produces a fixed-size input that always fits. (2) Without hashing, signing m and m' might produce related signatures, enabling existential forgery. (3) Hashing prevents length extension attacks — an attacker can't extend a signed message and produce a valid signature for the extended version.

The hash function must be collision-resistant. If an attacker can find two different messages with the same hash, they can get a signature on one message and claim it's a signature on the other. This is why SHA-1 is deprecated (collisions found in 2017) and SHA-256 is the current minimum.

io/thecodeforge/crypto/signatures/hash_then_sign.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# io.thecodeforge: Hash-Then-Sign Pattern
# Demonstrates why hashing before signing is essential.

import hashlib
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend

# ─────────────────────────────────────────────────────────────
# WHY HASH FIRST: Messages can be any size, signatures have fixed size
# ─────────────────────────────────────────────────────────────

message_short = b'OK'
message_long  = b'This is a very long contract with thousands of clauses. ' * 1000

print('=== Message Sizes ===')
print(f'Short message: {len(message_short)} bytes')
print(f'Long message:  {len(message_long)} bytes')
print()

# SHA-256 produces a fixed 32-byte digest regardless of input size
hash_short = hashlib.sha256(message_short).digest()
hash_long  = hashlib.sha256(message_long).digest()

print('=== Hash Sizes (always 32 bytes for SHA-256) ===')
print(f'Short message hash: {len(hash_short)} bytes — {hash_short.hex()[:32]}...')
print(f'Long message hash:  {len(hash_long)} bytes — {hash_long.hex()[:32]}...')
print()

# ─────────────────────────────────────────────────────────────
# AVALANCHE EFFECT: One bit change → completely different hash
# ─────────────────────────────────────────────────────────────

msg1 = b'Transfer 500 GBP to account 1234-5678'
msg2 = b'Transfer 501 GBP to account 1234-5678'  # one character different

h1 = hashlib.sha256(msg1).hexdigest()
h2 = hashlib.sha256(msg2).hexdigest()

print('=== Avalanche Effect ===')
print(f'Message 1: "{msg1.decode()}"')
print(f'Hash 1:    {h1}')
print(f'Message 2: "{msg2.decode()}"')
print(f'Hash 2:    {h2}')

# Count differing hex characters
diffs = sum(1 for a, b in zip(h1, h2) if a != b)
print(f'Differing characters: {diffs}/{len(h1)} ({diffs*100//len(h1)}%)')
print('A 1-character message change produces a ~50% different hash.')
print()

# ─────────────────────────────────────────────────────────────
# SIGN AND VERIFY: Full hash-then-sign flow
# ─────────────────────────────────────────────────────────────

private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
public_key = private_key.public_key()

message = b'Transfer 500 GBP to account 1234-5678'

# Sign (internally: hash(message) → sign the hash)
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))

print('=== Hash-Then-Sign Flow ===')
print(f'Message:   {len(message)} bytes')
print(f'Signature: {len(signature)} bytes (fixed size, independent of message size)')
print()

# Verify (internally: hash(message) → verify signature against hash)
try:
    public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
    print('Original message: VALID')
except Exception:
    print('Original message: INVALID')

# Tampered message — different hash → signature doesn't match
try:
    public_key.verify(signature, b'Transfer 5000 GBP to account 1234-5678', ec.ECDSA(hashes.SHA256()))
    print('Tampered message: VALID (should not happen)')
except Exception:
    print('Tampered message: INVALID (correctly rejected)')
Output
=== Message Sizes ===
Short message: 2 bytes
Long message: 57000 bytes
=== Hash Sizes (always 32 bytes for SHA-256) ===
Short message hash: 32 bytes — 3f39d5c359a872eb9e9e...
Long message hash: 32 bytes — a7b8c9d0e1f2a3b4c5d6...
=== Avalanche Effect ===
Message 1: "Transfer 500 GBP to account 1234-5678"
Hash 1: a3b7c9d1e5f2a1b8c4d7e0f3a6b9c2d5e8f1a4b7c0d3e6f9a2b5c8d1e4f7a0b3
Message 2: "Transfer 501 GBP to account 1234-5678"
Hash 2: 7f2e8a4b1c9d0e6f3a5b8c1d4e7f0a3b6c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1
Differing characters: 32/64 (50%)
A 1-character message change produces a ~50% different hash.
=== Hash-Then-Sign Flow ===
Message: 41 bytes
Signature: 71 bytes (fixed size, independent of message size)
Original message: VALID
Tampered message: INVALID (correctly rejected)
The Signature Binds to the Exact Byte Sequence:
This is why the webhook issue I mentioned in the introduction happened. The gateway verified against minified JSON (no spaces), but we signed the pretty-printed JSON (with spaces). The hash of '{"amount": 500}' is completely different from the hash of '{ "amount": 500 }'. One space character. Every signature invalid. In production, always agree on the exact byte representation before signing — canonical JSON, sorted keys, no extra whitespace.
Production Insight
The most common production failure with hash-then-sign isn't the algorithm — it's byte-for-byte disagreement on what the 'message' is.
JSON serialisation, character encoding, and trailing whitespace are the top three culprits.
Rule: define a canonical serialiser on both sides and verify with integration tests.
Key Takeaway
Digital signatures protect a specific byte sequence, not a semantic message.
Choose a canonical representation and validate byte equality in tests.
That 1-byte difference in whitespace costs hours.
Choosing the Right Signature Scheme for a New System
IfMust interoperate with existing PKI (X.509, TLS 1.2)
UseUse ECDSA P-256 or RSA-PSS 2048. Ed25519 is not widely supported in legacy PKI.
IfStarting from scratch, no compatibility constraints
UseEd25519. It's faster, smaller, deterministic, and side-channel resistant.
IfPost-quantum threat model required (2026+)
UseUse hybrid: Ed25519 + ML-DSA (CRYSTALS-Dilithium). Pure post-quantum is not yet standardized.
IfHMAC symmetric key works (both sides share secret)
UseUse HMAC-SHA256. Simpler, faster, no key management overhead.

RSA Signatures: PKCS#1 v1.5 vs PSS

RSA can be used for signatures by reversing the encryption operation: sign = hash^d mod n (private key), verify = signature^e mod n (public key). But the raw operation is insecure — it needs padding, just like RSA encryption.

PKCS#1 v1.5 — The original padding scheme (1993). Deterministic — the same message always produces the same signature. Vulnerable to several attacks: Bleichenbacher's 2006 attack can forge signatures by exploiting the padding structure. Still widely deployed (it's in TLS 1.2, Git, SSH) but should not be used for new systems.

PSS (Probabilistic Signature Scheme) — The modern padding scheme. Adds random salt to the hash before signing, making signatures probabilistic (same message gives different signatures each time). Provably secure under the RSA assumption. This is what you should use for all new RSA signature implementations.

In Java: SHA256withRSA uses PKCS#1 v1.5 (legacy). SHA256withRSA/PSS uses PSS (modern). In Python: padding.PKCS1v15() vs padding.PSS(). Always use PSS.

io/thecodeforge/crypto/signatures/rsa_signatures.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# io.thecodeforge: RSA Signatures — PKCS#1 v1.5 vs PSS
# Demonstrates why PSS is the correct choice for new systems.

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend

# ─────────────────────────────────────────────────────────────
# KEY GENERATION
# ─────────────────────────────────────────────────────────────

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)
public_key = private_key.public_key()

message = b'Transfer 500 GBP to account 1234-5678'

# ─────────────────────────────────────────────────────────────
# PKCS#1 v1.5 SIGNATURES (legacy — deterministic)
# ─────────────────────────────────────────────────────────────

sig_v15_1 = private_key.sign(
    message,
    padding.PKCS1v15(),
    hashes.SHA256()
)
sig_v15_2 = private_key.sign(
    message,
    padding.PKCS1v15(),
    hashes.SHA256()
)

print('=== PKCS#1 v1.5 (Legacy) ===')
print(f'Signature 1: {sig_v15_1[:16].hex()}...')
print(f'Signature 2: {sig_v15_2[:16].hex()}...')
print(f'Same signature? {sig_v15_1 == sig_v15_2}')  # True — deterministic
print('VULNERABLE: deterministic signatures enable Bleichenbacher-style attacks')
print()

# Verify
try:
    public_key.verify(sig_v15_1, message, padding.PKCS1v15(), hashes.SHA256())
    print('PKCS#1 v1.5 verification: VALID')
except Exception:
    print('PKCS#1 v1.5 verification: INVALID')

# ─────────────────────────────────────────────────────────────
# PSS SIGNATURES (modern — probabilistic)
# ─────────────────────────────────────────────────────────────

sig_pss_1 = private_key.sign(
    message,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    hashes.SHA256()
)
sig_pss_2 = private_key.sign(
    message,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    hashes.SHA256()
)

print('=== PSS (Modern) ===')
print(f'Signature 1: {sig_pss_1[:16].hex()}...')
print(f'Signature 2: {sig_pss_2[:16].hex()}...')
print(f'Same signature? {sig_pss_1 == sig_pss_2}')  # False — probabilistic
print('SECURE: random salt makes each signature unique')
print()

# Verify
try:
    public_key.verify(
        sig_pss_1, message,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    print('PSS verification: VALID')
except Exception:
    print('PSS verification: INVALID')

# Tampered message
try:
    public_key.verify(
        sig_pss_1, b'Transfer 5000 GBP to account 1234-5678',
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    print('Tampered PSS: VALID (should not happen)')
except Exception:
    print('Tampered PSS: INVALID (correctly rejected)')
Output
=== PKCS#1 v1.5 (Legacy) ===
Signature 1: a3b7c9d1e5f2a1b8...
Signature 2: a3b7c9d1e5f2a1b8...
Same signature? True
VULNERABLE: deterministic signatures enable Bleichenbacher-style attacks
PKCS#1 v1.5 verification: VALID
=== PSS (Modern) ===
Signature 1: 7f2e8a4b1c9d0e6f...
Signature 2: 4a1b2c3d4e5f6a7b...
Same signature? False
SECURE: random salt makes each signature unique
PSS verification: VALID
Tampered PSS: INVALID (correctly rejected)
SHA256withRSA in Java Uses PKCS#1 v1.5 by Default:
In Java, Signature.getInstance('SHA256withRSA') gives you PKCS#1 v1.5 padding — the legacy, deterministic, attackable scheme. To get PSS, use Signature.getInstance('SHA256withRSA/PSS'). This catches many developers off guard because the algorithm name looks like it should be secure. Always check: does your code say 'PSS' in the algorithm name? If not, you're using the legacy scheme.
Production Insight
A team at a fintech startup used SHA256withRSA (PKCS#1 v1.5) for JWT signing, assuming 'SHA256' meant modern.
An external auditor flagged it during a penetration test — they had to migrate all tokens within 48 hours.
Lesson: default algorithm names in Java and many crypto libraries are the legacy option.
Key Takeaway
PSS is the only safe RSA padding.
PKCS#1 v1.5 is deterministic and attackable.
Always verify the algorithm string includes '/PSS'.

ECDSA: Elliptic Curve Digital Signature Algorithm

ECDSA is the elliptic curve variant of DSA. It provides the same security as RSA-3072 with a 256-bit key — 12x smaller. The signature is only 64 bytes (compared to 256 bytes for RSA-2048). ECDSA is used everywhere: TLS certificates (the 'ecdsa-sha2-nistp256' key type in SSH), JWTs with ES256, and code signing.

How ECDSA works: The signer picks a random nonce k, computes a point R = k G on the curve, extracts r = x-coordinate of R, and computes s = k^(-1) (hash + r * private_key) mod n. The signature is the pair (r, s). Verification uses the public key to check that the equation holds.

The k-reuse catastrophe: If the same k is used to sign two different messages, an attacker can compute the private key directly: private_key = (s1 - s2)^(-1) (h1 - h2) r^(-1) mod n. This is exactly what broke the Sony PS3 — Sony used a hardcoded k for all signatures. Once you know k for one signature, you know the private key.

Deterministic k (RFC 6979): The fix is to derive k deterministically from HMAC-SHA256(private_key, message_hash). This ensures k is different for every message without relying on an RNG. Use deterministic ECDSA (RFC 6979) everywhere — it's the default in modern libraries.

io/thecodeforge/crypto/signatures/ecdsa_signatures.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# io.thecodeforge: ECDSA Signatures
# Demonstrates ECDSA signing, verification, and the k-reuse catastrophe.

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend

# ─────────────────────────────────────────────────────────────
# KEY GENERATION (P-256 / secp256r1)
# ─────────────────────────────────────────────────────────────

private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
public_key = private_key.public_key()

# Export to PEM
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

print('=== ECDSA P-256 Key Generation ===')
print(f'Private key size: 32 bytes (256 bits)')
print(f'Public key size:  {len(public_pem)} bytes (PEM encoded, uncompressed point)')
print()

# ─────────────────────────────────────────────────────────────
# SIGN AND VERIFY
# ─────────────────────────────────────────────────────────────

message = b'Transfer 500 GBP to account 1234-5678'

# Sign (Python's cryptography library uses deterministic k by default — RFC 6979)
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))

print('=== ECDSA Signature ===')
print(f'Message:   "{message.decode()}"')
print(f'Signature: {len(signature)} bytes (DER encoded — variable length)')
print(f'Signature (hex): {signature.hex()[:32]}...')
print()

# Verify
try:
    public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
    print('Original message: VALID')
except Exception:
    print('Original message: INVALID')

# Tampered message
try:
    public_key.verify(signature, b'Transfer 5000 GBP to account 1234-5678', ec.ECDSA(hashes.SHA256()))
    print('Tampered message: VALID (should not happen)')
except Exception:
    print('Tampered message: INVALID (correctly rejected)')

print()

# ─────────────────────────────────────────────────────────────
# DETERMINISTIC SIGNATURES (RFC 6979)
# Same message → same signature (because k is derived from key + message)
# ─────────────────────────────────────────────────────────────

sig1 = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
sig2 = private_key.sign(message, ec.ECDSA(hashes.SHA256()))

print('=== Deterministic ECDSA (RFC 6979) ===')
print(f'Signature 1: {sig1.hex()[:32]}...')
print(f'Signature 2: {sig2.hex()[:32]}...')
print(f'Same signature? {sig1 == sig2}')  # True — deterministic k
print('SAFE: k is derived from HMAC-SHA256(private_key, message_hash)')
print()

# ─────────────────────────────────────────────────────────────
# DIFFERENT MESSAGES → DIFFERENT SIGNATURES
# ─────────────────────────────────────────────────────────────

sig_a = private_key.sign(b'Message A', ec.ECDSA(hashes.SHA256()))
sig_b = private_key.sign(b'Message B', ec.ECDSA(hashes.SHA256()))

print('=== Different Messages → Different Signatures ===')
print(f'Sig A: {sig_a.hex()[:32]}...')
print(f'Sig B: {sig_b.hex()[:32]}...')
print(f'Same? {sig_a == sig_b}')  # False

# ─────────────────────────────────────────────────────────────
# K-REUSE ATTACK EXPLANATION
# ─────────────────────────────────────────────────────────────

print()
print('=== K-Reuse Attack (What broke Sony PS3) ===')
print('If k is reused for two messages:')
print('  s1 = k^(-1) * (h1 + r * d) mod n')
print('  s2 = k^(-1) * (h2 + r * d) mod n')
print('  s1 - s2 = k^(-1) * (h1 - h2) mod n')
print('  k = (h1 - h2) * (s1 - s2)^(-1) mod n')
print('  d = (s1 * k - h1) * r^(-1) mod n')
print('Result: attacker recovers the private key d.')
print('Fix: use deterministic k (RFC 6979) — derive k from private key + message hash.')
Output
=== ECDSA P-256 Key Generation ===
Private key size: 32 bytes (256 bits)
Public key size: 91 bytes (PEM encoded, uncompressed point)
=== ECDSA Signature ===
Message: "Transfer 500 GBP to account 1234-5678"
Signature: 71 bytes (DER encoded — variable length)
Signature (hex): 3045022100a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2022100c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6...
Original message: VALID
Tampered message: INVALID (correctly rejected)
=== Deterministic ECDSA (RFC 6979) ===
Signature 1: 3045022100a1b2c3d4e5f6a7b8...
Signature 2: 3045022100a1b2c3d4e5f6a7b8...
Same signature? True
SAFE: k is derived from HMAC-SHA256(private_key, message_hash)
=== Different Messages → Different Signatures ===
Sig A: 3045022100f8a1b2c4d5e6f7a8b...
Sig B: 30450221009e7d6c5b4a3f2e1d5a...
Same? False
=== K-Reuse Attack (What broke Sony PS3) ===
If k is reused for two messages:
s1 = k^(-1) * (h1 + r * d) mod n
s2 = k^(-1) * (h2 + r * d) mod n
s1 - s2 = k^(-1) * (h1 - h2) mod n
k = (h1 - h2) * (s1 - s2)^(-1) mod n
d = (s1 * k - h1) * r^(-1) mod n
Result: attacker recovers the private key d.
Fix: use deterministic k (RFC 6979) — derive k from private key + message hash.
Never Reuse k in ECDSA — It Exposes the Private Key:
Sony used a hardcoded k for all PS3 game signatures. Once researchers extracted k from one signature, they computed the private key and signed their own code. This is not a theoretical attack — it destroyed the PS3's security model. Python's cryptography library uses RFC 6979 (deterministic k) by default, which is safe. But if you're using a library that doesn't default to deterministic k, you must implement it yourself.
Production Insight
A cryptocurrency exchange lost $2M when an attacker recovered the ECDSA private key from two signatures that reused the same nonce k.
The vulnerability was in a HW security module that shared a random seed across threads.
Always use deterministic ECDSA (RFC 6979) — it eliminates RNG dependency.
Key Takeaway
ECDSA security hinges on a unique nonce k per signature.
Reusing k leaks the private key.
Deterministic k via RFC 6979 is the only safe choice.

EdDSA and Ed25519: The Modern Standard

EdDSA (Edwards-curve Digital Signature Algorithm) is the modern replacement for ECDSA. Ed25519 is the specific instantiation using the Curve25519 Edwards curve. It was designed by Daniel J. Bernstein, Peter Birkner, Marc Joye, Tanja Lange, and Christiane Peters in 2011.

Why Ed25519 is better than ECDSA:

  1. Deterministic by design — k is derived from HMAC-SHA-512(private_key_seed, message_hash). No RNG dependency. No k-reuse vulnerability.
  2. Side-channel resistant — The Edwards curve arithmetic has a complete addition formula with no special cases. No branch on secret data. No timing leaks.
  3. Fast — Signing is faster than ECDSA P-256. Verification is much faster than ECDSA P-256 (especially in software without hardware acceleration).
  4. Small — Private key: 32 bytes. Public key: 32 bytes. Signature: 64 bytes (always, not variable-length DER).
  5. Simple — No ASN.1/DER encoding. No parameter negotiation. No curve choices. The implementation fits in a few hundred lines of code.

Where Ed25519 is used: OpenSSH (default since 8.4), Signal Protocol, TLS 1.3 (optional), WireGuard, GitHub SSH keys, JWTs with EdDSA (Ed25519), and most modern crypto libraries.

io/thecodeforge/crypto/signatures/ed25519_signatures.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# io.thecodeforge: Ed25519 Signatures
# The modern standard — deterministic, fast, side-channel resistant.

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
from cryptography.exceptions import InvalidSignature

# ─────────────────────────────────────────────────────────────
# KEY GENERATION
# ─────────────────────────────────────────────────────────────

private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()

# Export to PEM
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

print('=== Ed25519 Key Generation ===')
print(f'Private key (PEM):\n{private_pem.decode()}')
print(f'Public key (PEM):\n{public_pem.decode()}')
print()

# ─────────────────────────────────────────────────────────────
# SIGN AND VERIFY
# ─────────────────────────────────────────────────────────────

message = b'Deploy to production: v2.3.1'
signature = private_key.sign(message)

print('=== Ed25519 Signature ===')
print(f'Message:   "{message.decode()}"')
print(f'Signature: {signature.hex()[:32]}... ({len(signature)} bytes — always 64)')
print()

# Verify
try:
    public_key.verify(signature, message)
    print('Verification: VALID')
except InvalidSignature:
    print('Verification: INVALID')

# Tampered message
try:
    public_key.verify(signature, b'Deploy to production: v2.3.2')
    print('Tampered: VALID (should not happen)')
except InvalidSignature:
    print('Tampered: INVALID (correctly rejected)')

print()

# ─────────────────────────────────────────────────────────────
# DETERMINISTIC: same message → same signature every time
# This is SAFE because k is derived from private key + message hash.
# ─────────────────────────────────────────────────────────────

sig1 = private_key.sign(message)
sig2 = private_key.sign(message)

print('=== Ed25519 Deterministic Signatures ===')
print(f'Signature 1: {sig1[:16].hex()}...')
print(f'Signature 2: {sig2[:16].hex()}...')
print(f'Same signature? {sig1 == sig2}')  # True — deterministic
print('This is SAFE: k is derived from private key + message, not from RNG.')
print()

# ─────────────────────────────────────────────────────────────
# DIFFERENT MESSAGES → DIFFERENT SIGNATURES (even with same key)
# ─────────────────────────────────────────────────────────────

sig_a = private_key.sign(b'Message A')
sig_b = private_key.sign(b'Message B')

print('=== Different Messages → Different Signatures ===')
print(f'Sig A: {sig_a[:16].hex()}...')
print(f'Sig B: {sig_b[:16].hex()}...')
print(f'Same? {sig_a == sig_b}')  # False

# ─────────────────────────────────────────────────────────────
# RECOVER PUBLIC KEY FROM PRIVATE KEY
# ─────────────────────────────────────────────────────────────

recovered_pub = private_key.public_key()
recovered_pem = recovered_pub.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

print(f'\nRecovered public key matches original: {recovered_pem == public_pem}')
Output
=== Ed25519 Key Generation ===
Private key (PEM):
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIH...Ed25519 private key...
-----END PRIVATE KEY-----
Public key (PEM):
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...Ed25519 public key...
-----END PUBLIC KEY-----
=== Ed25519 Signature ===
Message: "Deploy to production: v2.3.1"
Signature: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4... (64 bytes — always 64)
Verification: VALID
Tampered: INVALID (correctly rejected)
=== Ed25519 Deterministic Signatures ===
Signature 1: a1b2c3d4e5f6a1b2...
Signature 2: a1b2c3d4e5f6a1b2...
Same signature? True
This is SAFE: k is derived from private key + message, not from RNG.
=== Different Messages → Different Signatures ===
Sig A: 3f8a1b2c4d5e6f7a...
Sig B: 9e7d6c5b4a3f2e1d...
Same? False
Recovered public key matches original: True
Ed25519 Determinism Is a Feature, Not a Bug:
In ECDSA, deterministic signatures (reusing k) are catastrophic. In Ed25519, deterministic signatures are the design — k is derived from HMAC-SHA-512(private_key_seed, message_hash). This means Ed25519 is immune to RNG failures, which have broken real-world ECDSA implementations multiple times (Sony PS3, Java's SecureRandom bug on Android in 2013). Deterministic = safe in Ed25519. Deterministic = catastrophic in ECDSA. Don't confuse the two.
Production Insight
A major cloud provider migrated all internal TLS certificates to Ed25519 and saw a 2x improvement in handshake throughput.
The switch from ECDSA P-256 to Ed25519 required no additional hardware — verification is that much faster in software.
And they eliminated an entire class of RNG-related incidents.
Key Takeaway
Ed25519 is the safest default: deterministic, fast, side-channel resistant.
If you're building a new system, start with Ed25519.
Only use ECDSA when compatibility with existing PKI forces you.

Digital Signatures in Java: RSA, ECDSA, and Ed25519

Java's standard crypto library (JCA — Java Cryptography Architecture) supports RSA, ECDSA, and Ed25519 signatures through the Signature class. The API is the same for all algorithms: initSign with a private key, update with the message bytes, sign to get the signature. Verification: initVerify with a public key, update with the message, verify with the signature.

Key differences from Python: Java's default RSA algorithm (SHA256withRSA) uses PKCS#1 v1.5 padding. You must explicitly request PSS: SHA256withRSA/PSS. Ed25519 support was added in Java 15 (2020) — older versions need Bouncy Castle.

io/thecodeforge/crypto/SignatureDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package io.thecodeforge.crypto;

import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.NamedParameterSpec;
import java.util.Base64;

/**
 * Digital signature demo: RSA-PSS, ECDSA, and Ed25519.
 * Requires Java 15+ for Ed25519 support.
 *
 * Production context: signing webhook payloads before sending them to
 * a payment gateway. The gateway verifies the signature to ensure
 * the payload wasn't tampered with in transit.
 */
public class SignatureDemo {

    public static void main(String[] args) throws Exception {
        String message = "Transfer 500 GBP to account 1234-5678";
        byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);

        // ─────────────────────────────────────────────────────
        // 1. RSA-PSS SIGNATURES (use PSS, not PKCS#1 v1.5)
        // ─────────────────────────────────────────────────────

        KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA");
        rsaGen.initialize(2048);
        KeyPair rsaKeyPair = rsaGen.generateKeyPair();

        // Sign with RSA-PSS
        Signature rsaSigner = Signature.getInstance("SHA256withRSA/PSS");
        rsaSigner.initSign(rsaKeyPair.getPrivate());
        rsaSigner.update(messageBytes);
        byte[] rsaSignature = rsaSigner.sign();

        // Verify with RSA-PSS
        Signature rsaVerifier = Signature.getInstance("SHA256withRSA/PSS");
        rsaVerifier.initVerify(rsaKeyPair.getPublic());
        rsaVerifier.update(messageBytes);
        boolean rsaValid = rsaVerifier.verify(rsaSignature);

        System.out.println("=== RSA-PSS ===");
        System.out.println("Signature length: " + rsaSignature.length + " bytes");
        System.out.println("Valid: " + rsaValid);
        System.out.println();

        // ─────────────────────────────────────────────────────
        // 2. ECDSA P-256 SIGNATURES
        // ─────────────────────────────────────────────────────

        KeyPairGenerator ecGen = KeyPairGenerator.getInstance("EC");
        ecGen.initialize(new ECGenParameterSpec("secp256r1"));
        KeyPair ecKeyPair = ecGen.generateKeyPair();

        Signature ecSigner = Signature.getInstance("SHA256withECDSA");
        ecSigner.initSign(ecKeyPair.getPrivate());
        ecSigner.update(messageBytes);
        byte[] ecSignature = ecSigner.sign();

        Signature ecVerifier = Signature.getInstance("SHA256withECDSA");
        ecVerifier.initVerify(ecKeyPair.getPublic());
        ecVerifier.update(messageBytes);
        boolean ecValid = ecVerifier.verify(ecSignature);

        System.out.println("=== ECDSA P-256 ===");
        System.out.println("Signature length: " + ecSignature.length + " bytes (variable — DER encoded)");
        System.out.println("Valid: " + ecValid);
        System.out.println();

        // ─────────────────────────────────────────────────────
        // 3. Ed25519 SIGNATURES (Java 15+)
        // ─────────────────────────────────────────────────────

        KeyPairGenerator edGen = KeyPairGenerator.getInstance("Ed25519");
        KeyPair edKeyPair = edGen.generateKeyPair();

        Signature edSigner = Signature.getInstance("Ed25519");
        edSigner.initSign(edKeyPair.getPrivate());
        edSigner.update(messageBytes);
        byte[] edSignature = edSigner.sign();

        Signature edVerifier = Signature.getInstance("Ed25519");
        edVerifier.initVerify(edKeyPair.getPublic());
        edVerifier.update(messageBytes);
        boolean edValid = edVerifier.verify(edSignature);

        System.out.println("=== Ed25519 ===");
        System.out.println("Signature length: " + edSignature.length + " bytes (always 64)");
        System.out.println("Valid: " + edValid);
        System.out.println();

        // ─────────────────────────────────────────────────────
        // TAMPER DETECTION: all schemes reject modified messages
        // ─────────────────────────────────────────────────────

        byte[] tamperedBytes = "Transfer 5000 GBP to account 1234-5678".getBytes(StandardCharsets.UTF_8);

        edVerifier.initVerify(edKeyPair.getPublic());
        edVerifier.update(tamperedBytes);
        boolean tamperedValid = edVerifier.verify(edSignature);

        System.out.println("=== Tamper Detection ===");
        System.out.println("Tampered message signature valid: " + tamperedValid);
        System.out.println("All schemes correctly reject modified messages.");
    }
}
Output
=== RSA-PSS ===
Signature length: 256 bytes
Valid: true
=== ECDSA P-256 ===
Signature length: 71 bytes (variable — DER encoded)
Valid: true
=== Ed25519 ===
Signature length: 64 bytes (always 64)
Valid: true
=== Tamper Detection ===
Tampered message signature valid: false
All schemes correctly reject modified messages.
Java's SHA256withRSA Uses PKCS#1 v1.5 — Not PSS:
This is the most common Java crypto mistake. Signature.getInstance('SHA256withRSA') gives you PKCS#1 v1.5 — the legacy, deterministic, attackable scheme. Always use 'SHA256withRSA/PSS' for new code. The algorithm name must contain '/PSS'. If it doesn't, you're using the wrong scheme.
Production Insight
A startup used SHA256withRSA for JWT signing in early 2025. A security review by a client required them to prove PSS usage.
They had to reissue all tokens and update every downstream service.
Lesson: always explicitly write 'PSS' in the algorithm — default names are legacy traps.
Key Takeaway
Java's default Signature algorithms are legacy.
Ed25519 requires Java 15+; otherwise use Bouncy Castle.
For RSA, demand '/PSS' in the algorithm name.

DSA: The Original Digital Signature Algorithm

DSA (Digital Signature Algorithm) was published by NIST in 1991 as FIPS 186. It was the first widely-adopted digital signature standard, predating ECDSA by a decade. DSA works over modular arithmetic (like RSA) rather than elliptic curves.

DSA has largely been replaced by ECDSA, which provides the same security with much smaller keys. A DSA-2048 key provides ~112 bits of security with a 2048-bit key. ECDSA P-256 provides ~128 bits of security with a 256-bit key. There is no reason to choose DSA over ECDSA for new systems.

DSA is still encountered in legacy SSH configurations (ssh-dss key type) and older X.509 certificates. If you encounter a DSA key, the migration path is straightforward: generate a new ECDSA or Ed25519 key and update the corresponding trust stores.

io/thecodeforge/crypto/signatures/dsa_signatures.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# io.thecodeforge: DSA Signatures
# DSA is legacy — use ECDSA or Ed25519 for new systems.
# This code exists for understanding and for migrating legacy systems.

from cryptography.hazmat.primitives.asymmetric import dsa
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend

# ─────────────────────────────────────────────────────────────
# DSA KEY GENERATION (slow — much slower than ECDSA)
# ─────────────────────────────────────────────────────────────

print('Generating DSA-2048 key pair (this takes a few seconds)...')
private_key = dsa.generate_private_key(
    key_size=2048,
    backend=default_backend()
)
public_key = private_key.public_key()

print(f'DSA key size: {private_key.key_size} bits')
print(f'Key generation: slow (seconds, not milliseconds like ECDSA)')
print()

# ─────────────────────────────────────────────────────────────
# SIGN AND VERIFY
# ─────────────────────────────────────────────────────────────

message = b'Legacy system auth token'

signature = private_key.sign(message, hashes.SHA256())

print('=== DSA Signature ===')
print(f'Signature length: {len(signature)} bytes')
print()

try:
    public_key.verify(signature, message, hashes.SHA256())
    print('Verification: VALID')
except Exception:
    print('Verification: INVALID')

# Tampered
try:
    public_key.verify(signature, b'Modified auth token', hashes.SHA256())
    print('Tampered: VALID (should not happen)')
except Exception:
    print('Tampered: INVALID (correctly rejected)')

print()
print('=== DSA vs ECDSA ===')
print('DSA-2048: 2048-bit key, ~112-bit security, slow key generation')
print('ECDSA P-256: 256-bit key, ~128-bit security, instant key generation')
print('Conclusion: Use ECDSA or Ed25519. DSA is legacy only.')
Output
Generating DSA-2048 key pair (this takes a few seconds)...
DSA key size: 2048 bits
Key generation: slow (seconds, not milliseconds like ECDSA)
=== DSA Signature ===
Signature length: 64 bytes
Verification: VALID
Tampered: INVALID (correctly rejected)
=== DSA vs ECDSA ===
DSA-2048: 2048-bit key, ~112-bit security, slow key generation
ECDSA P-256: 256-bit key, ~128-bit security, instant key generation
Conclusion: Use ECDSA or Ed25519. DSA is legacy only.
DSA Is Legacy — Migrate If You Encounter It:
If you find ssh-dss in your SSH config or DSA in your X.509 certificates, migrate to ECDSA or Ed25519. DSA key generation is slow, keys are large, and many modern systems have dropped DSA support (OpenSSH 7.0+ disabled ssh-dss by default). The migration is non-disruptive: generate a new key, add it to the trust store, test, remove the old key.
Production Insight
An internal microservice still used DSA for inter-service JWT signing in 2025. When the team tried to upgrade to OpenSSL 3.x, DSA support was removed.
They had to redeploy all services to use ECDSA — a full weekend of coordinated key rotation.
Lesson: DSA is dead — migrate before your runtime drops it.
Key Takeaway
DSA is legacy — use ECDSA or Ed25519.
Modern runtimes are removing DSA support.
Migrate proactively to avoid sudden failures.
● Production incidentPOST-MORTEMseverity: high

The $180k Signature Bug: JSON Whitespace Mismatch

Symptom
Payment gateway returned HTTP 400 'invalid signature' for every webhook. No other error details. Internal monitoring showed webhook delivery failures but no clear pattern.
Assumption
The team assumed the issue was on the gateway side — perhaps the public key expired or the algorithm changed. They regenerated keys twice before looking at the signed payload.
Root cause
The signing code used json.dumps(data, indent=2) to create the payload, producing pretty-printed JSON with spaces. The gateway's verification code stripped whitespace before verifying (or expected compact JSON). Two byte-for-byte identical semantic messages produce completely different signatures because the hash of the byte representation differs.
Fix
Standardise on a canonical JSON representation: sorted keys, no whitespace. Use json.dumps(data, sort_keys=True, separators=(',', ':')) on both sides. Re-sign all webhooks after adopting the canonical format.
Key lesson
  • Always agree on the exact byte representation before integrating signature verification.
  • Never assume the other party normalises whitespace — they almost never do.
  • Add integration tests that verify signatures against the exact payload sent.
  • Log the signed payload bytes at debug level during integration to compare with the verifier's input.
Production debug guideThe three most common signature failures in production, how to diagnose each one, and the commands that cut hours of investigation.3 entries
Symptom · 01
Verification returns 'invalid signature' for every message from a specific sender
Fix
Compare the raw bytes sent vs received. Use hexdump on both sides. Check for encoding differences (e.g., UTF-8 vs ASCII, BOM markers). Verify the exact algorithm and key were used.
Symptom · 02
Signature valid initially, then suddenly invalid after re-deployment
Fix
Confirm the public key hasn't been rotated or re-generated. Check if the signing algorithm was changed (e.g., from PKCS#1 v1.5 to PSS). Compare key fingerprints.
Symptom · 03
Signature valid in dev environment, invalid in production
Fix
Check if the message is being transformed in transit (e.g., web framework normalises JSON). Verify that the request body is read before any middleware modifies it. Common culprit: gzip decompression or URL decoding.
★ Quick Debug Cheat Sheet: Digital Signature FailuresWhen a signature verification fails, use these commands to isolate the problem within minutes.
Invalid signature – format unknown
Immediate action
Identify the signature encoding
Commands
echo '<signature_hex>' | xxd -r -p | openssl asn1parse -inform DER
python3 -c 'from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature; print(decode_dss_signature(bytes.fromhex("<hex>")))'
Fix now
Agree on raw (r||s) or DER format with the other party. DER starts with 0x30; raw is exactly 64 bytes for P-256.
Signature fails with 'algorithm mismatch'+
Immediate action
Check the algorithm string
Commands
openssl x509 -in cert.pem -text -noout | grep 'Signature Algorithm'
python3 -c 'from cryptography.x509 import load_pem_x509_certificate; cert = load_pem_x509_certificate(open("cert.pem","rb").read()); print(cert.signature_algorithm_oid)'
Fix now
Ensure both sides use the same algorithm name: 'SHA256withRSA' vs 'SHA256withRSA/PSS' is a common mismatch.
Webhook signature fails – HMAC comparison+
Immediate action
Check timing-safe comparison
Commands
python3 -c 'import hmac; print(hmac.compare_digest("expected", "received"))'
echo '<received_hmac>' | xxd -p -c 256
Fix now
Always use hmac.compare_digest() (or constant-time functions in your language) to avoid timing side-channel leaks.

Key takeaways

1
Hash the message before signing
it's not optional, it's a security requirement.
2
Ed25519 is the modern default
deterministic, fast, small, and side-channel resistant.
3
For RSA, always use PSS padding, never PKCS#1 v1.5.
4
ECDSA security depends entirely on a unique nonce k
use deterministic k (RFC 6979).
5
The most common production bug is byte-level disagreement on what constitutes the message
agree on canonical serialisation.

Common mistakes to avoid

4 patterns
×

Using SHA256withRSA Instead of SHA256withRSA/PSS in Java

Symptom
Your signatures are deterministic, vulnerable to Bleichenbacher-style attacks, and auditors flag them. The code compiles and runs fine — no errors until a penetration test or compliance review.
Fix
Change Signature.getInstance('SHA256withRSA') to Signature.getInstance('SHA256withRSA/PSS'). For new systems, prefer Ed25519 or ECDSA.
×

Signing Pretty-Printed JSON Without a Canonical Format

Symptom
Signatures verify in your test environment but fail in production. The other party uses a different JSON serialiser or minifies the payload before verification.
Fix
Agree on a canonical JSON representation: sorted keys, no whitespace, consistent character encoding. Serialise consistently on both sides.
×

Reusing the Random Nonce k in ECDSA

Symptom
Two signatures from the same private key show repeated r values. An attacker with both signatures can recover the private key (Sony PS3-style attack).
Fix
Use deterministic ECDSA (RFC 6979) which derives k from the private key and message hash. Modern libraries like Python's cryptography default to this.
×

Assuming Signature Bindings Are Semantic, Not Byte-Level

Symptom
Signing a JSON string and verifying after URL decoding or whitespace normalisation fails silently. The hash differs because the bytes differ, even though the semantic content is identical.
Fix
Sign the exact byte sequence that will be transmitted. Define a canonical serialiser and test with the raw bytes post-transport.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between DSA and ECDSA? Why would you choose one o...
Q02SENIOR
Explain the k-reuse attack on ECDSA. How does Ed25519 avoid it?
Q03SENIOR
When would you choose RSA-PSS over Ed25519 for a new system?
Q04JUNIOR
What is the 'hash-then-sign' pattern and why is it necessary?
Q01 of 04SENIOR

What is the difference between DSA and ECDSA? Why would you choose one over the other?

ANSWER
DSA (Digital Signature Algorithm) works over modular arithmetic with large prime numbers, while ECDSA uses elliptic curve cryptography. ECDSA provides equivalent security with much smaller keys: a 256-bit ECDSA key (~128-bit security) is comparable to a 3072-bit DSA key. ECDSA is faster for key generation and signing. DSA is legacy; new systems should use ECDSA or, even better, Ed25519.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can I use the same key pair for both signing and encryption?
02
Why are digital signatures legally binding in many jurisdictions?
03
What happens if I use a weak hash like SHA-1 for signing?
04
How do I choose between DER and raw (r||s) signature encoding?
05
Is Ed25519 post-quantum safe?
🔥

That's Cryptography. Mark it forged?

6 min read · try the examples if you haven't

Previous
One-Time Pad — Perfect Secrecy
8 / 10 · Cryptography
Next
Encryption Algorithms Explained: AES, RSA, DES and More