Code signing is how software publishers prove they wrote the code and that it hasn’t been tampered with since.
Let’s watch a piece of code get signed and then verified. Imagine we have a simple Python script, hello.py:
print("Hello, world!")
To sign this, we need a code signing certificate, which is essentially a digital ID issued by a trusted Certificate Authority (CA). For this example, we’ll use a self-signed certificate (not trusted by default, but good for demonstration).
First, we generate a private key and a self-signed certificate. On Linux/macOS:
openssl req -x509 -newkey rsa:2048 -keyout private.key -out certificate.crt -days 365 -nodes -subj "/CN=MyCompany"
This creates private.key (the secret key) and certificate.crt (the public certificate). The openssl command is the workhorse here. It uses the RSA algorithm to generate a 2048-bit key pair. The -days 365 sets the certificate’s validity period, and -nodes means we don’t encrypt the private key with a passphrase (for simplicity in this example, but highly discouraged in production). The -subj "/CN=MyCompany" sets the Common Name to "MyCompany," identifying the signer.
Now, we need to create a manifest of the files we want to sign. If hello.py were part of a larger package with other files, we’d list them all. For our single file:
echo "hello.py" > manifest.txt
Next, we create a digest (a unique fingerprint) of hello.py and store it in the manifest.
sha256sum hello.py | awk '{print $1}' > hello.sha256
cat hello.sha256 >> manifest.txt
echo "hello.sha256" >> manifest.txt
This uses sha256sum to compute the SHA-256 hash of hello.py. We then append this hash to manifest.txt, followed by the name of the hash file itself. The manifest.txt now looks something like:
hello.py
a1b2c3d4e5f6... (the actual hash of hello.py)
hello.sha256
This manifest is what we will actually sign. We create a digest of manifest.txt:
sha256sum manifest.txt | awk '{print $1}' > manifest.sha256
Finally, we use our private key to encrypt the digest of the manifest. This encrypted digest is the digital signature.
openssl dgst -sha256 -sign private.key -out manifest.sha256.sig manifest.txt
This command takes the SHA-256 digest of manifest.txt, uses private.key to sign it, and outputs the signature to manifest.sha256.sig.
The signed package now consists of:
hello.py(the original code)certificate.crt(the public certificate)manifest.txt(the list of files and their hashes)hello.sha256(the hash ofhello.py)manifest.sha256(the hash ofmanifest.txt)manifest.sha256.sig(the actual digital signature)
When a user downloads this package and wants to verify it, the process is reversed. The verifier needs the public part of the signing process: the code itself, the certificate, and the signature.
First, the verifier checks the certificate.crt. If it’s self-signed, the OS or application will warn the user that the publisher is unknown. If it were signed by a trusted CA, the OS would confirm its authenticity. The verifier also checks if the certificate has expired or been revoked.
Assuming the certificate is deemed trustworthy, the verifier re-calculates the digest of manifest.txt using the same algorithm (SHA-256 in this case).
sha256sum manifest.txt | awk '{print $1}' > verifier_manifest.sha256
Then, it uses the public key embedded within certificate.crt to decrypt manifest.sha256.sig.
openssl dgst -sha256 -verify certificate.crt -signature manifest.sha256.sig manifest.txt
If the decrypted value matches the verifier_manifest.sha256 that was just calculated, it means manifest.txt has not been altered since it was signed.
The verifier then reads manifest.txt. For each file listed (e.g., hello.py), it re-calculates its hash.
sha256sum hello.py | awk '{print $1}' > verifier_hello.sha256
It then compares this verifier_hello.sha256 with the hash stored in manifest.txt (which was hello.sha256). If they match, it confirms that hello.py has not been tampered with.
This whole process provides assurance that the software comes from the claimed publisher and hasn’t been modified by an attacker.
The most surprising part is that code signing doesn’t inherently prevent malicious code from being distributed; it only helps users identify if the code they are about to run is from a trusted source and hasn’t been altered. A trusted source can still distribute malicious code, and an attacker could, in theory, get their own code signed by a legitimate but compromised certificate authority or trick a user into accepting a self-signed certificate. The trust chain is everything.
The next step is understanding how operating systems and browsers integrate these checks, often automatically, and what happens when those checks fail.