Skip to content

How Does Signing a Message Prevent Tampering? (And Why Encryption Alone Isn't Enough)

Problem

When I first learned about cryptography, I thought encrypting a message was enough to keep it secure. But then I encountered a troubling question: what if someone changes the encrypted message?

I assumed encryption protected everything—confidentiality, integrity, authenticity. That assumption was wrong.

Here’s the core problem: encryption only keeps your message secret. It does NOT prevent someone from tampering with it.

Environment

  • Python 3.10+ (cryptography library)
  • Node.js 18+ (crypto module)
  • Go 1.21+ (crypto/hmac package)

What happened?

I was building a system to send financial instructions between services. My plan was:

  1. Encrypt the message with the recipient’s public key
  2. Send the encrypted message
  3. Recipient decrypts with their private key

Simple, right? But I realized something was missing. If an attacker intercepted my encrypted message, they could:

  • Replace it with a previously captured valid encrypted message (replay attack)
  • Flip bits in predictable ways if the encryption mode wasn’t authenticated
  • Resend the same message multiple times

Even though they couldn’t read the message, they could still cause damage by manipulating it.

Here’s why encryption alone fails:

Encryption security gaps
┌─────────────────┐
│ Attacker │
│ (Can't read) │
│ But CAN: │
│ - Replace │
│ - Replay │
│ - Bit-flip │
└────────┬────────┘
┌─────────────────┐ ┌─────────────────┐
│ Encrypted │ ──► │ Recipient │
│ Message │ │ Decrypts: OK │
│ (Tampered!) │ │ But WRONG msg! │
└─────────────────┘ └─────────────────┘

The key insight from a Reddit discussion:

“When you sign a message you don’t encrypt it. It remains in plain text. You just attach a hash with it that the receiver can use to verify that the message was not changed.” — plastikmissile

And another comment clarified the real purpose:

“It doesn’t prevent. It makes CHECKING for tampering evident.” — kschang

How to solve it?

The solution is digital signatures. But here’s what surprised me: signatures don’t prevent tampering either—they make it detectable.

How Digital Signatures Work

Sender side:

Signing process (sender)
1. Message: "Transfer $100 to Alice"
2. Hash the message: SHA-256("Transfer $100 to Alice") = 0x7f3a...
3. Encrypt hash with private key: RSA_Sign(private_key, 0x7f3a...) = SIGNATURE
4. Send: Message + SIGNATURE

Receiver side:

Verification process (receiver)
1. Receive: "Transfer $100 to Alice" + SIGNATURE
2. Hash the received message: SHA-256("Transfer $100 to Alice") = 0x7f3a...
3. Decrypt signature with public key: RSA_Verify(public_key, SIGNATURE) = 0x7f3a...
4. Compare: 0x7f3a... == 0x7f3a... → VERIFIED

If someone tampers:

Tampering detection
Attacker changes message to: "Transfer $10000 to Bob"
1. New hash: SHA-256("Transfer $10000 to Bob") = 0xb4c1...
2. Attacker tries to use old signature: SIGNATURE
3. Decrypt signature: RSA_Verify(public_key, SIGNATURE) = 0x7f3a...
4. Compare: 0xb4c1... != 0x7f3a... → TAMPERING DETECTED

The attacker can’t forge a new signature because they don’t have the sender’s private key.

Python Example: RSA Signatures

signature_demo.py
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
# Generate key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# Sign a message
message = b"Transfer $100 to Alice"
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# Verify the signature (receiver side)
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("Signature is valid - message is authentic and intact")
except Exception:
print("Signature verification failed - message may be tampered")
# Tampering attempt
tampered_message = b"Transfer $10000 to Bob"
try:
public_key.verify(
signature,
tampered_message, # Different message!
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
except Exception:
print("Tampering detected! Signature doesn't match.")

JavaScript/Node.js Example: ECDSA Signatures

signature_demo.js
const crypto = require('crypto');
// Generate ECDSA key pair
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'secp256k1'
});
// Sign a message
const message = 'Transfer $100 to Alice';
const sign = crypto.createSign('SHA256');
sign.update(message);
sign.end();
const signature = sign.sign(privateKey, 'hex');
console.log('Signature:', signature);
// Verify the signature
const verify = crypto.createVerify('SHA256');
verify.update(message);
verify.end();
const isValid = verify.verify(publicKey, signature, 'hex');
console.log('Valid:', isValid); // true
// Tampering detection
const tamperVerify = crypto.createVerify('SHA256');
tamperVerify.update('Transfer $10000 to Bob'); // Different message
tamperVerify.end();
const isTampered = tamperVerify.verify(publicKey, signature, 'hex');
console.log('Tampered message valid:', isTampered); // false

The reason

I think the key reason for the confusion is that people conflate three different security properties:

PropertyWhat It MeansEncryptionDigital Signature
ConfidentialityOnly authorized parties can readYesNo (message is plaintext)
IntegrityMessage wasn’t alteredNoYes
AuthenticityProof of who sent itNoYes
Non-repudiationSender can’t deny sendingNoYes

Encryption keeps secrets. Signatures prove authenticity and detect tampering. They’re complementary, not alternatives.

Common Mistakes I Made

Mistake 1: Confusing signing with encrypting

  • Signing: Encrypts hash with your private key, message stays plaintext
  • Encrypting: Encrypts message with recipient’s public key

Mistake 2: Thinking signatures prevent tampering

  • They don’t prevent—they detect
  • The message can still be changed, but verification will fail

Mistake 3: Using encryption without authentication

  • ECB, CBC, CTR modes alone don’t provide integrity
  • Always use authenticated encryption (AES-GCM, ChaCha20-Poly1305)

Mistake 4: Not checking the signature

  • Receiving a signed message without verifying is useless
  • Always verify before trusting

Summary

In this post, I explained why encryption alone cannot prevent message tampering and how digital signatures make tampering detectable. The key point is that signatures create a tamper-evident seal by encrypting a hash of your message with your private key—anyone can verify the message came from you and wasn’t altered, but they can’t forge a new signature without your private key.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments