Skip to content
Home DSA Digital Signatures — JSON Whitespace Broke Our Webhooks

Digital Signatures — JSON Whitespace Broke Our Webhooks

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Cryptography → Topic 8 of 10
A $180k webhook failed because json.
🔥 Advanced — solid DSA foundation required
In this tutorial, you'll learn
A $180k webhook failed because json.
  • Hash the message before signing — it's not optional, it's a security requirement.
  • Ed25519 is the modern default: deterministic, fast, small, and side-channel resistant.
  • For RSA, always use PSS padding, never PKCS#1 v1.5.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
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.
🚨 START HERE

Quick Debug Cheat Sheet: Digital Signature Failures

When a signature verification fails, use these commands to isolate the problem within minutes.
🟡

Invalid signature – format unknown

Immediate ActionIdentify 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 NowAgree 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 ActionCheck 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 NowEnsure both sides use the same algorithm name: 'SHA256withRSA' vs 'SHA256withRSA/PSS' is a common mismatch.
🟡

Webhook signature fails – HMAC comparison

Immediate ActionCheck timing-safe comparison
Commands
python3 -c 'import hmac; print(hmac.compare_digest("expected", "received"))'
echo '<received_hmac>' | xxd -p -c 256
Fix NowAlways use hmac.compare_digest() (or constant-time functions in your language) to avoid timing side-channel leaks.
Production Incident

The $180k Signature Bug: JSON Whitespace Mismatch

A payment gateway silently dropped all webhooks because the signature verification expected minified JSON while the service signed pretty-printed JSON. One space character per key cost $180k in delayed payments.
SymptomPayment gateway returned HTTP 400 'invalid signature' for every webhook. No other error details. Internal monitoring showed webhook delivery failures but no clear pattern.
AssumptionThe 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 causeThe 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.
FixStandardise 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 Guide

The three most common signature failures in production, how to diagnose each one, and the commands that cut hours of investigation.

Verification returns 'invalid signature' for every message from a specific senderCompare 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.
Signature valid initially, then suddenly invalid after re-deploymentConfirm 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.
Signature valid in dev environment, invalid in productionCheck 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.

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.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
# 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.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
# 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.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
# 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.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
# 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.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
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.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
# 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.

🎯 Key Takeaways

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

⚠ Common Mistakes to Avoid

    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 Questions on This Topic

  • QWhat is the difference between DSA and ECDSA? Why would you choose one over the other?Mid-levelReveal
    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.
  • QExplain the k-reuse attack on ECDSA. How does Ed25519 avoid it?SeniorReveal
    In ECDSA, each signature requires a unique random nonce k. If the same k is used to sign two different messages, an attacker can compute the private key by solving: k = (h1 - h2) (s1 - s2)^(-1) mod n, then d = (s1 k - h1) * r^(-1) mod n. Ed25519 avoids this by design: its nonce is derived deterministically from HMAC-SHA-512(private_key_seed, message_hash), so k is always unique per message and no RNG is needed.
  • QWhen would you choose RSA-PSS over Ed25519 for a new system?SeniorReveal
    You would choose RSA-PSS if you must interoperate with legacy PKI that requires RSA (e.g., X.509 certificates in many enterprise environments, TLS 1.2, or compliance mandates). Otherwise, Ed25519 is superior: faster, smaller signatures (64 bytes vs 256 bytes for RSA-2048), deterministic, and side-channel resistant by design. If post-quantum security is a concern, consider a hybrid Ed25519 + ML-DSA approach.
  • QWhat is the 'hash-then-sign' pattern and why is it necessary?JuniorReveal
    Hash-then-sign means the message is hashed first (e.g., with SHA-256) and the resulting digest is signed, rather than signing the full message. It's necessary because: (1) asymmetric signature schemes like RSA and ECDSA can only handle inputs up to the key size — hashing produces a fixed-size digest that always fits. (2) Without hashing, signing related messages could leak information, enabling existential forgery. (3) Hashing prevents length extension attacks. It also allows signing arbitrarily large messages efficiently.

Frequently Asked Questions

Can I use the same key pair for both signing and encryption?

Technically yes for RSA, but it's strongly discouraged. The security properties differ: signing uses the private key to create a verifiable message, encryption uses the public key to encrypt a message that only the private key can decrypt. Using the same key for both increases the attack surface and complicates key management. Always use separate key pairs for signing and encryption.

Why are digital signatures legally binding in many jurisdictions?

Digital signatures provide authentication (the signer is who they claim), integrity (the document hasn't been altered), and non-repudiation (the signer can't deny signing). These properties satisfy legal requirements for electronic signatures under laws like the US ESIGN Act and EU eIDAS regulation. The specific algorithm and implementation must meet security standards to be admissible.

What happens if I use a weak hash like SHA-1 for signing?

SHA-1 is cryptographically broken — collisions have been demonstrated (SHAttered attack, 2017). An attacker can generate two documents with the same SHA-1 hash, get you to sign one, and claim the signature applies to the other. This undermines non-repudiation and integrity. Always use SHA-256 or stronger for digital signatures.

How do I choose between DER and raw (r||s) signature encoding?

DER encoding is required by X.509 certificates and many standards (e.g., TLS). Raw (r||s) encoding is simpler, fixed-size (64 bytes for P-256), and often used in JWTs (JOSE) or custom protocols. Choose based on the receiving system's expected format. The most common production bug is a mismatch — always document the wire format explicitly.

Is Ed25519 post-quantum safe?

No. Ed25519 (like all elliptic curve and RSA schemes) is vulnerable to Shor's algorithm on a sufficiently large quantum computer. For post-quantum security, use a hybrid scheme combining Ed25519 with a NIST-approved post-quantum algorithm such as ML-DSA (CRYSTALS-Dilithium). NIST standardised ML-DSA in 2024, and hybrid deployments are recommended from 2026 onwards.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousOne-Time Pad — Perfect SecrecyNext →Encryption Algorithms Explained: AES, RSA, DES and More
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged