๐ Introduction
In the first two parts of this series, I explored how to secure file transfers using SHA-256 checksums for integrity and then took it a step further with HMAC-SHA256, which added authenticity through a shared secret key. These approaches work well in trusted environments, especially for internal or on-prem systems.
But what happens when the systems are not in the same secure network, or when you need to ensure that even without a shared secret, the fileโs integrity and the senderโs identity can be verified? Thatโs where Digital Signatures come into play.
Digital signatures, built on algorithms like RSA (RivestโShamirโAdleman) and ECDSA (Elliptic Curve Digital Signature Algorithm), bring two powerful guarantees:
- Integrity โ ensuring the file hasnโt been tampered with.
- Authenticity โ proving that the file truly came from the claimed sender.
In this part, Iโll explore how digital signatures fit into secure file transfers, compare RSA and ECDSA, and walk through generating and verifying signatures with code examples.
๐ What Are Digital Signatures?
- A digital signature is like a virtual fingerprint for a file.
- It ensures that the file has not been tampered with (integrity).
- It ensures that the file truly comes from the claimed sender (authenticity).
- It works using a private key (to sign) and a public key (to verify).
โ๏ธ How It Works (Step-by-Step)
- Sender generates a hash of the file (e.g., SHA-256).
- Sender encrypts the hash with their private key โ digital signature.
- The file + signature are sent to the receiver.
- Receiver generates their own hash of the received file.
- Receiver decrypts the signature using senderโs public key to retrieve the original hash.
- If both hashes match โ the file is authentic and untampered.
๐ How to Generate Key Pairs
To use digital signatures, you need a key pair:
- Private Key (kept secret, used for signing).
- Public Key (shared, used for verifying).
There are many ways to generate the key pairs. The common and straightforward way is to use the openssl library. Here I provide the Python way.
๐ Generating RSA Key Pairs
# Generate RSA Public-Private Key
def generate_rsa_key(private_key_file, public_key_file):
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
# Save Private Key
with open(private_key_file, "wb") as fout:
fout.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM, # Format = PEM
format=serialization.PrivateFormat.TraditionalOpenSSL, # Structure - OpenSSL style
encryption_algorithm=serialization.NoEncryption() # No password protection
))
# Save Public Key
public_key = private_key.public_key()
with open(public_key_file, "wb") as fout:
fout.write(public_key.public_bytes(
encoding=serialization.Encoding.PEM, # Format = PEM
format=serialization.PublicFormat.SubjectPublicKeyInfo # Standard X.509 format
))
print("RSA key generation complete")
๐ Generating ECDSA Key Pairs
# Generate ECDSA Key Pair
def generate_ec_key(private_key_file, public_key_file):
# Generate ECDSA Private Key
private_key = ec.generate_private_key(ec.SECP256R1()) # Specifies which Elliptic Curve to use
# Uses the curve known as prime256v1 or NIST P-256.
# Save Private Key
with open(private_key_file, "wb") as fout:
fout.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
# Save Public Key
public_key = private_key.public_key()
with open(public_key_file, "wb") as fout:
fout.write(public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
))
print("EC key generation complete")
โ RSA vs ECDSA Quick Note
- RSA โ Widely used, mature, simpler to understand, but keys/signatures are larger.
- ECDSA โ Faster, smaller keys, but more complex math. Popular in modern systems (TLS, blockchain).
A comparison table of RSA vs ECDSA is provided below for information.
Once the Key Pairs are generated and saved, the next step is to generate the Digital Signature.
๐ Signing the File
def generate_digital_signature(private_key_file, file_path, signature_file_path):
# Load File Content
with open(file_path, "rb") as fin:
data = fin.read()
# Read the Private Key from pem file
with open(private_key_file, "rb") as fout:
private_key = serialization.load_pem_private_key(
fout.read(),
password=None
)
# Sign the Data
signature = private_key.sign(
data,
padding.PSS(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# Save the Signature
with open(signature_file_path, "wb") as fout:
fout.write(signature)
print("Signature generation complete")
Let's understand how the signing works.
- private_key.sign( โฆ. ) :
- Uses the RSA private key to generate a digital signature.
- Input is the raw data (in bytes) you want to sign.
- The result (signature) is a unique cryptographic value tied to both the data and the private key.
- padding.PSS(โฆ) : Provides Padding Schemes for Security
- PSS (Probabilistic Signature Scheme) is used , which is the modern recommended padding for RSA signatures.
- It makes each signature different, even if the same data is signed multiple times (unlike older, deterministic schemes).
- Inside PSS:
- mgf=padding.MGF1(hashes.SHA256()) โ MGF1 is a mask generation function that adds randomness, using SHA-256 internally.
- salt_length=padding.PSS.MAX_LENGTH โ Uses the largest possible salt (random value) to maximize security.
- hashes.SHA256()
- Before signing, the file content is hashed using SHA-256.
- Instead of signing the entire raw file (which could be GBs in size), RSA signs this fixed-length hash digest.
- This ensures efficiency and security โ even tiny changes in the file create a completely different hash, and thus a different signature.
โก Plain-English Analogy
Think of this like stamping a document with a unique wax seal:
- The document = your file (data).
- The stamp mold = your private key.
- The wax pattern (randomized via PSS) = padding randomness.
- The final wax seal impression = the signature.
Anyone with the public key can check the seal and confirm:
- The file hasnโt been changed.
- It really came from the holder of the private key.
๐ Verifying the File
# Verify the File with the Signature
def verify_file(public_key_file, file_path, file_signature_path):
# Load Public Key
with open(public_key_file, "rb") as fin:
public_key = serialization.load_pem_public_key(
fin.read(),
backend=default_backend()
)
# Load File Signature
with open(file_signature_path, "rb") as fin:
signature = fin.read()
# Load File Content
with open(file_path, "rb") as fin:
data = fin.read()
# Verify the Signature
try:
public_key.verify(
signature=signature,
data=data,
padding=padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
algorithm=hashes.SHA256()
)
print("Signature verified")
except Exception as e:
print("Signature verification failed")
print(f"Exception: {e}")
โ Pros and Cons
Let's understand the Pros and Cons of this approach.
Pros:
- Strong authenticity (no shared secret needed).
- Works across untrusted networks.
- Non-repudiation: Sender cannot deny signing.
Cons:
- Slower than checksum or HMAC.
- Requires secure key management.
- More complex setup compared to symmetric approaches.
๐ When to Use Digital Signatures?
- When files are shared across different organizations.
- When authenticity is critical (legal, financial, healthcare files).
- When compliance demands non-repudiation (e.g., contracts, audit logs).
๐ Conclusion
Digital signatures add a powerful layer of security for file transfers โ going beyond integrity to authenticity and trust. They are the go-to choice when sharing files in untrusted or external environments.
โก๏ธ In the next part of this series, Iโll look at AES Encryption for File Transfers to ensure not just authenticity, but also confidentiality.
The code provided above can be found in Github.
Top comments (0)