Python Digital Signatures: Sign and Verify Data Safely
Digital signatures are cryptographic proofs of authorship and integrity. A sender signs a message using their private key; recipients verify the signature using the sender's public key. If the signature is valid, the recipient knows the message came from the claimed sender and was not tampered with. Digital signatures are used to sign software releases, API requests, blockchain transactions, and any data that requires non-repudiation (the sender cannot deny having sent it).
A digital signature is not the same as a handwritten signature. It is a mathematical proof cryptographically linked to both the message and the signer's private key. It cannot be forged or reused (each message produces a unique signature).
How Digital Signatures Work
A digital signature is created by hashing the message and then encrypting the hash with the sender's private key. Recipients verify by decrypting the signature with the sender's public key and comparing the result to the hash of the received message.
Sign:
Message → Hash (SHA-256) → Hash value
Hash value + Private Key → Signature (encrypted hash)
Verify:
Signature + Public Key → Decrypt to get Hash value
Received Message → Hash (SHA-256) → Computed hash
If Hash value == Computed hash: Signature is valid
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
# Generate an RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
public_key = private_key.public_key()
# Message to sign
message = b"Release version 1.2.3 - compiled on 2026-06-02"
# Sign the message with the private key
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print(f"Signature: {signature.hex()}")
# Verify the signature with the public key
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 unmodified.")
except Exception:
print("Signature is invalid! Message may have been tampered with.")
# Demonstrate tampering detection
tampered_message = b"Release version 1.2.4 - HACKED"
try:
public_key.verify(
signature,
tampered_message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("This should not print!")
except Exception:
print("Tampering detected! Signature verification failed.")
PSS vs. PKCS1v15: Padding Schemes for Signatures
RSA signatures require a padding scheme to be secure. Two common schemes are PSS and PKCS1v15.
PSS (Probabilistic Signature Scheme) is the modern standard. It adds randomness to the signature, so the same message signed twice produces different signatures. PSS is more secure theoretically and is recommended for new code.
PKCS1v15 is older and deterministic (same message → same signature). It is still secure in practice but has known weakness against certain attacks. PKCS1v15 is widely used for legacy compatibility (X.509 certificates, TLS).
For new code in 2026, use PSS.
| Property | PSS | PKCS1v15 |
|---|---|---|
| Randomness | Probabilistic (different each time) | Deterministic (same each time) |
| Security | Modern, proven secure | Legacy, proven secure in practice |
| Use case | New applications | Compatibility, X.509 certificates |
| TLS usage | TLS 1.3+ (recommended) | TLS 1.2 (deprecated) |
Production Pattern: Sign and Verify with Message
Here's a complete pattern for signing and verifying messages:
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
import json
class MessageSigner:
"""Sign and verify messages using RSA-2048 with PSS padding."""
@staticmethod
def generate_keypair():
"""Generate a new RSA-2048 key pair."""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
return private_key, private_key.public_key()
@staticmethod
def sign_message(private_key, message: bytes) -> bytes:
"""Sign a message and return the signature."""
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return signature
@staticmethod
def verify_signature(public_key, message: bytes, signature: bytes) -> bool:
"""Verify a signature. Return True if valid, False otherwise."""
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except Exception:
return False
@staticmethod
def save_private_key(private_key, filepath: str, password: bytes = None):
"""Save private key to file (optionally encrypted)."""
encryption = serialization.NoEncryption()
if password:
encryption = serialization.BestAvailableEncryption(password)
pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=encryption
)
with open(filepath, "wb") as f:
f.write(pem)
@staticmethod
def load_private_key(filepath: str, password: bytes = None):
"""Load private key from file."""
with open(filepath, "rb") as f:
pem = f.read()
return serialization.load_pem_private_key(
pem, password=password, backend=default_backend()
)
@staticmethod
def save_public_key(public_key, filepath: str):
"""Save public key to file."""
pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
with open(filepath, "wb") as f:
f.write(pem)
@staticmethod
def load_public_key(filepath: str):
"""Load public key from file."""
with open(filepath, "rb") as f:
pem = f.read()
return serialization.load_pem_public_key(pem, backend=default_backend())
# Example: Sign and verify a software release
private_key, public_key = MessageSigner.generate_keypair()
release_info = {
"version": "1.2.3",
"timestamp": "2026-06-02T10:30:00Z",
"sha256": "abc123def456...",
"changelog": "Fixed security vulnerability CVE-2026-1234"
}
message = json.dumps(release_info, sort_keys=True).encode()
# Sign
signature = MessageSigner.sign_message(private_key, message)
print(f"Signature: {signature.hex()[:50]}...")
# Verify
is_valid = MessageSigner.verify_signature(public_key, message, signature)
print(f"Signature valid: {is_valid}") # True
# Verify fails if message is tampered
tampered = message.replace(b"1.2.3", b"1.2.4")
is_valid = MessageSigner.verify_signature(public_key, tampered, signature)
print(f"Tampered message valid: {is_valid}") # False
Real-World Use Case: Signing API Requests
API clients often sign requests to prove they are authorized and unmodified. The server verifies the signature using the client's public key.
import json
import time
from typing import Dict, Any
class SignedAPIRequest:
"""Represent an API request signed with the client's private key."""
def __init__(self, private_key, public_key):
self.private_key = private_key
self.public_key = public_key
def create_request(self, method: str, path: str, body: Dict[str, Any] = None):
"""Create a signed API request."""
# Construct the request payload
timestamp = int(time.time())
payload = {
"method": method,
"path": path,
"timestamp": timestamp,
"body": body or {}
}
# Canonicalize (deterministic JSON)
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
# Sign
signature = MessageSigner.sign_message(self.private_key, canonical)
return {
"payload": payload,
"signature": signature.hex()
}
def verify_request(self, request: Dict) -> bool:
"""Verify the signature on an API request."""
canonical = json.dumps(
request["payload"],
sort_keys=True,
separators=(",", ":")
).encode()
signature = bytes.fromhex(request["signature"])
return MessageSigner.verify_signature(self.public_key, canonical, signature)
# Example
private_key, public_key = MessageSigner.generate_keypair()
api = SignedAPIRequest(private_key, public_key)
# Client sends signed request
request = api.create_request("POST", "/api/transfer", {"amount": 1000, "to": "[email protected]"})
print(f"Request: {json.dumps(request, indent=2, default=str)[:100]}...")
# Server verifies
is_valid = api.verify_request(request)
print(f"Request signature valid: {is_valid}") # True
Digital Signatures in Code Signing and Release Management
Code signing is critical for software distribution. A developer signs executable files or release artifacts with their private key. Users download the file and verify the signature using the developer's public key (distributed in a certificate), ensuring the software came from the claimed developer and was not modified in transit.
This prevents man-in-the-middle attacks during download and protects against supply-chain attacks where malicious code is injected after release.
Key Takeaways
- Digital signatures prove authorship and integrity: only the holder of the private key can create a valid signature; anyone with the public key can verify it.
- A signature is created by hashing the message and encrypting the hash with the sender's private key; verification decrypts the signature and compares hashes.
- Use PSS padding for new code (probabilistic, theoretically modern); PKCS1v15 is for legacy compatibility.
- Digital signatures enable non-repudiation: the signer cannot deny having signed a message (legally binding in many jurisdictions).
- Applications: code signing, API request authentication, blockchain transactions, software releases.
Frequently Asked Questions
Can I reuse a signature on a different message?
No. A signature is mathematically linked to a specific message. Reusing a signature on a different message will fail verification (unless the messages hash to the same value, which is computationally infeasible with SHA-256).
What is the difference between a signature and a hash?
A hash is one-way and provides no proof of authorship. A signature is created by encrypting a hash with a private key, proving the holder of that key approved the message. Hashes are public; signatures prove ownership of a private key.
How long does a signature remain valid?
Indefinitely, from a cryptographic perspective. However, if the signer's private key is compromised, all signatures created with that key become untrustworthy. In practice, signatures are timestamped and keys are rotated, so old signatures are re-evaluated periodically.
Can I sign large files efficiently?
Yes. Instead of signing the file directly, sign a hash of the file (called a digest signature). This is fast and practical for gigabyte-sized files. The server computes the file hash and verifies the signature on the hash.
What is a code signing certificate?
A code signing certificate is an X.509 certificate containing a public key, signed by a Certificate Authority (CA). It cryptographically binds a developer's identity to a public key. When users download a signed application, they can verify the CA's signature on the certificate, proving the application came from the claimed developer.