Digital Signatures: Proving Who Sent the Message
1. Why Should You Care?
You download a software update. How do you know itโs really from the vendor and not malware?
You receive an email from your bank. How do you know itโs really from your bank?
You sign a contract electronically. How does the court know you actually agreed?
Digital signatures solve these problems. Theyโre the foundation of software security, secure communications, and legal electronic documents.
2. Definition
A digital signature is a cryptographic scheme that proves:
- Authentication: The message was created by the claimed sender
- Integrity: The message hasnโt been modified since signing
- Non-repudiation: The sender cannot deny having signed the message
Physical signature vs Digital signature:
Physical:
- Can be forged by copying
- Doesn't detect document modification
- Looks the same regardless of document
Digital:
- Mathematically tied to signer's private key
- Any modification invalidates signature
- Different for every document3. How Digital Signatures Work
The Basic Process
Signing:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Hash the message hash = SHA256(message) โ
โ 2. Sign the hash signature = Sign(hash, privKey) โ
โ 3. Send message + signature โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Verification:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Hash the message hash = SHA256(message) โ
โ 2. Verify signature Verify(signature, hash, pubKey) โ
โ 3. If valid, message is authentic and unmodified โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโWhy Hash First?
Why not sign the message directly?
1. Performance:
- RSA can only sign ~256 bytes
- Signing 1MB file would need thousands of operations
- Hashing reduces any size to 32 bytes
2. Security:
- Signing raw data has mathematical weaknesses
- Hash provides additional security layer
3. Standardization:
- Fixed-size input to signature algorithm
- Consistent behavior regardless of message size4. Signature Algorithms
RSA Signatures (RSA-PSS)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
# Generate keys
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
message = b"Contract: I agree to pay $1000"
# Sign with PSS padding (recommended)
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# Verify
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("Signature valid: Message is authentic")
except Exception:
print("Signature invalid: Message may be forged or modified")ECDSA (Elliptic Curve DSA)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
# Generate keys (much smaller than RSA)
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
message = b"Transfer 1 BTC to address xyz"
# Sign
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
# Verify
try:
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
print("Valid ECDSA signature")
except Exception:
print("Invalid signature")EdDSA (Ed25519) - The Modern Choice
from cryptography.hazmat.primitives.asymmetric import ed25519
# Generate keys
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()
message = b"Authenticate this request"
# Sign (algorithm is built-in, no choices to make)
signature = private_key.sign(message)
# Verify
try:
public_key.verify(signature, message)
print("Valid Ed25519 signature")
except Exception:
print("Invalid signature")Comparison
Algorithm | Key Size | Signature | Speed | Security
------------+----------+-----------+---------+----------
RSA-2048 | 256 B | 256 B | Slow | 112-bit
RSA-3072 | 384 B | 384 B | Slower | 128-bit
ECDSA P-256 | 64 B | 64 B | Fast | 128-bit
Ed25519 | 32 B | 64 B | Fastest | 128-bit
Recommendation: Use Ed25519 for new projects
Fallback: ECDSA P-256 for compatibility
Legacy: RSA-2048+ when required5. What Signatures Guarantee (and Donโt)
What They Guarantee
โ Authentication
"This was signed by the holder of private key X"
โ Integrity
"The message hasn't changed since signing"
โ Non-repudiation
"The signer cannot deny signing this exact message"What They DONโT Guarantee
โ Identity
"The signer is who they claim to be"
(You need certificates for this - next article)
โ Confidentiality
"The message is secret"
(Signatures don't encrypt - message is public)
โ Timestamp
"This was signed at time X"
(You need trusted timestamping)
โ Authorization
"The signer was authorized to sign this"
(Business logic, not cryptography)6. Common Use Cases
Code Signing
Why it matters:
- Proves software comes from claimed publisher
- Detects tampering (malware injection)
- OS can block unsigned/unknown software
How it works:
Developer User
โ โ
โ 1. Creates software โ
โ 2. Hashes executable โ
โ 3. Signs with private โ
โ key (from CA) โ
โ โ
โโโโโ signed.exe โโโโโโโโ>โ
โ โ
โ 4. OS verifies sig โ
โ 5. Checks cert chain โ
โ 6. Runs if valid โGit Commit Signing
# Configure signing key
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
# Sign a commit
git commit -S -m "Signed commit"
# Verify commits
git log --show-signature
# Output:
# commit abc123
# gpg: Signature made Wed Feb 1 2025 10:00:00
# gpg: using RSA key ABCD1234...
# gpg: Good signature from "Developer Name <[email protected]>"JWT (JSON Web Tokens)
import jwt
import datetime
# Server's private key
PRIVATE_KEY = """-----BEGIN EC PRIVATE KEY-----
...
-----END EC PRIVATE KEY-----"""
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----"""
# Create signed token
payload = {
'user_id': 123,
'role': 'admin',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
token = jwt.encode(payload, PRIVATE_KEY, algorithm='ES256')
print(f"Token: {token}")
# Verify token
try:
decoded = jwt.decode(token, PUBLIC_KEY, algorithms=['ES256'])
print(f"Valid token for user: {decoded['user_id']}")
except jwt.InvalidSignatureError:
print("Token signature invalid - possible tampering")
except jwt.ExpiredSignatureError:
print("Token expired")Document Signing (PDF)
# Using pyHanko for PDF signing (conceptual example)
from pyhanko.sign import signers
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
# Load PDF
with open('contract.pdf', 'rb') as f:
w = IncrementalPdfFileWriter(f)
# Sign with certificate
signer = signers.SimpleSigner.load(
'signing_key.pem',
'signing_cert.pem',
key_passphrase=b'password'
)
# Apply signature
signers.sign_pdf(
w,
signers.PdfSignatureMetadata(field_name='Signature1'),
signer=signer
)
with open('contract_signed.pdf', 'wb') as out:
w.write(out)7. Security Considerations
Never Sign Attacker-Controlled Data
# DANGEROUS: Signing arbitrary user input
def vulnerable_sign(user_data):
return private_key.sign(user_data) # Attacker controls content!
# SAFE: Sign your own structured data
def safe_sign(action, resource_id, timestamp):
# You control the structure and content
message = f"{action}:{resource_id}:{timestamp}".encode()
return private_key.sign(message)Signature Malleability
Some signature schemes are malleable:
Given a valid signature S for message M,
attacker can create S' that also validates for M.
ECDSA is malleable:
(r, s) and (r, -s mod n) both valid for same message
Impact:
- Usually not a problem
- Can cause issues in some protocols
- Bitcoin had vulnerabilities from this
Defense:
- Use deterministic signatures (Ed25519)
- Canonicalize signatures (low-s only)Key Management
Private key security is EVERYTHING:
If private key leaks:
- Attacker can sign as you
- All past signatures remain valid
- Must revoke and re-sign everything
Best practices:
- Store in HSM for high-value keys
- Use password-protected key files
- Rotate signing keys periodically
- Keep offline backups securely8. Timestamping
The Problem
Alice signs a document on Feb 1, 2025.
Her key expires on Mar 1, 2025.
On Apr 1, 2025:
- Signature is still mathematically valid
- But was it created before or after expiry?
- Alice could have signed after revocation!Trusted Timestamping
1. Alice creates signature
2. Alice sends signature to Timestamp Authority (TSA)
3. TSA signs: "I saw this signature at time X"
4. TSA returns timestamp token
Now we can prove:
- Signature existed at specific time
- Key was valid at that timeRFC 3161 Timestamping
# Conceptual example - actual implementation varies
import hashlib
import requests
def get_timestamp(signature_bytes):
# Create timestamp request
digest = hashlib.sha256(signature_bytes).digest()
# Send to TSA
response = requests.post(
'http://timestamp.example.com/tsa',
data=create_timestamp_request(digest),
headers={'Content-Type': 'application/timestamp-query'}
)
return parse_timestamp_response(response.content)
# The timestamp token proves when the signature was created9. Signature Verification in Practice
Complete Example
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import json
import base64
import time
class SignedMessage:
def __init__(self, private_key=None):
if private_key:
self.private_key = private_key
self.public_key = private_key.public_key()
else:
self.private_key = ed25519.Ed25519PrivateKey.generate()
self.public_key = self.private_key.public_key()
def sign(self, data: dict) -> dict:
"""Sign a message with metadata"""
# Add metadata
message = {
'data': data,
'timestamp': int(time.time()),
'signer': self._get_key_id()
}
# Serialize deterministically
message_bytes = json.dumps(message, sort_keys=True).encode()
# Sign
signature = self.private_key.sign(message_bytes)
return {
'message': message,
'signature': base64.b64encode(signature).decode()
}
def verify(self, signed_message: dict, public_key) -> dict:
"""Verify a signed message"""
message = signed_message['message']
signature = base64.b64decode(signed_message['signature'])
# Reconstruct the exact bytes that were signed
message_bytes = json.dumps(message, sort_keys=True).encode()
# Verify signature
public_key.verify(signature, message_bytes)
return message['data']
def _get_key_id(self) -> str:
"""Get a short identifier for the public key"""
pub_bytes = self.public_key.public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw
)
return base64.b64encode(pub_bytes[:8]).decode()
# Usage
signer = SignedMessage()
# Sign
signed = signer.sign({
'action': 'transfer',
'amount': 100,
'to': '[email protected]'
})
print("Signed message:")
print(json.dumps(signed, indent=2))
# Verify
try:
data = signer.verify(signed, signer.public_key)
print(f"Verified! Data: {data}")
except Exception as e:
print(f"Verification failed: {e}")10. Common Mistakes
| Mistake | Consequence | Correct Approach |
|---|---|---|
| Not verifying signatures | Accept forged messages | Always verify before trusting |
| Signing without hashing | Performance and security issues | Use standard signature schemes |
| Using MD5/SHA1 for signatures | Vulnerable to collision attacks | Use SHA-256 or SHA-3 |
| Storing private key in code | Key compromise | Use HSM, env vars, or key vault |
| Not checking key validity | Accept signatures from revoked keys | Check certificate chain |
| Confusing encryption with signing | Wrong security properties | They solve different problems |
11. Summary
Three things to remember:
Digital signatures prove authenticity and integrity. They guarantee who signed and that content wasnโt modified, but they donโt encrypt or prove real-world identity.
Use Ed25519 for new projects. Itโs fast, secure, and hard to misuse. Fall back to ECDSA P-256 for compatibility, RSA only when required.
Signatures need context. A valid signature only proves someone signed somethingโyou need certificates (PKI) to know who that someone is.
12. Whatโs Next
We can now verify that messages come from the holder of a specific private key. But how do we know that key belongs to who they claim to be?
In the next article: Certificates and PKIโhow we build trust chains on the internet and prove real-world identity.
