Luke a Pro

Luke Sun

Developer & Marketer

๐Ÿ‡บ๐Ÿ‡ฆ

Digital Signatures: Proving Who Sent the Message

| , 13 minutes reading.

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:

  1. Authentication: The message was created by the claimed sender
  2. Integrity: The message hasnโ€™t been modified since signing
  3. 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 document

3. 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 size

4. 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 required

5. 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 <dev@example.com>"

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 securely

8. 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 time

RFC 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 created

9. 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': 'user@example.com'
})

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

MistakeConsequenceCorrect Approach
Not verifying signaturesAccept forged messagesAlways verify before trusting
Signing without hashingPerformance and security issuesUse standard signature schemes
Using MD5/SHA1 for signaturesVulnerable to collision attacksUse SHA-256 or SHA-3
Storing private key in codeKey compromiseUse HSM, env vars, or key vault
Not checking key validityAccept signatures from revoked keysCheck certificate chain
Confusing encryption with signingWrong security propertiesThey solve different problems

11. Summary

Three things to remember:

  1. 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.

  2. 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.

  3. 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.