DEV Community

Cover image for From Hashes to Signatures: Securing File Transfers with RSA/ECDSA Digital Signatures
Aditya R
Aditya R

Posted on • Originally published at Medium

From Hashes to Signatures: Securing File Transfers with RSA/ECDSA Digital Signatures

๐Ÿ”‘ 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.

From fingerprints to handshakes to seals โ€” the evolution of securing file transfers: Checksum โ†’ HMAC โ†’ Digital Signatures.

๐Ÿ“Œ 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)

  1. Sender generates a hash of the file (e.g., SHA-256).
  2. Sender encrypts the hash with their private key โ†’ digital signature.
  3. The file + signature are sent to the receiver.
  4. Receiver generates their own hash of the received file.
  5. Receiver decrypts the signature using senderโ€™s public key to retrieve the original hash.
  6. If both hashes match โ†’ the file is authentic and untampered.

Overall Flow

๐Ÿ” 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")
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”‘ 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")
Enter fullscreen mode Exit fullscreen mode

โœ… 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.

RSA vs ECDSA

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")
Enter fullscreen mode Exit fullscreen mode

Let's understand how the signing works.

  1. 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.
  2. 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).
  3. 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.
  4. 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}")
Enter fullscreen mode Exit fullscreen mode

โœ… 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)