Software Signing


Why Sign Software?

Software signing verifies the origin and integrity of code. It ensures that artifacts haven't been tampered with and come from a trusted source.

GPG Signing

Traditional signing with PGP/GPG:




# Generate GPG key


gpg --full-generate-key


gpg --armor --export "developer@example.com" > public.key




# Sign artifacts


gpg --armor --detach-sign myapp.tar.gz


gpg --verify myapp.tar.gz.asc myapp.tar.gz




# Sign git commits


git config commit.gpgsign true


git config user.signingkey KEY_ID


git commit -S -m "Signed commit"








# Programmatic GPG verification


import gnupg




def verify_signature(artifact, signature_file):


gpg = gnupg.GPG()




with open(signature_file, "rb") as sf:


verified = gpg.verify_file(sf, artifact)




if verified.valid:


return {


"valid": True,


"fingerprint": verified.fingerprint,


"username": verified.username,


"timestamp": verified.timestamp


}


return {"valid": False}





Sigstore and cosign

Sigstore simplifies code signing with keyless options:




# Keyless signing with cosign


cosign sign myregistry.io/myapp:latest




# Sign with identity


cosign sign \


--identity-token $GITHUB_TOKEN \


ghcr.io/myorg/myapp@sha256:abc123




# Verify


cosign verify \


--certificate-identity "developer@example.com" \


--certificate-oidc-issuer "https://github.com/login/oauth" \


myregistry.io/myapp:latest








# Cosign in CI pipeline


jobs:


sign:


runs-on: ubuntu-latest


permissions:


id-token: write


packages: write




steps:


- uses: actions/checkout@v4


- uses: sigstore/cosign-installer@main




- name: Sign container image


run: |


cosign sign \


--yes \


ghcr.io/${{ github.repository }}@${{ steps.push.outputs.digest }}




- name: Sign SBOM


run: |


cosign attest-blob sbom.json \


--predicate sbom.json \


--type cyclonedx \


--yes





in-toto Attestations

in-toto provides end-to-end supply chain integrity:




# Create in-toto attestation


from in_toto_attestation.v1 import Statement, Attestation




def create_attestation(subject, predicate_type, predicate):


statement = Statement(


type="https://in-toto.io/Statement/v1",


subject=[{


"name": subject["name"],


"digest": subject["digest"]


}],


predicate_type=predicate_type,


predicate=predicate


)




return Attestation(


statement=statement,


signatures=[{


"sig": sign_statement(statement),


"keyid": get_key_id()


}]


)




# SLSA provenance attestation


provenance = create_attestation(


subject={"name": "myapp", "digest": {"sha256": "abc..."}},


predicate_type="https://slsa.dev/provenance/v1",


predicate={


"builder": {"id": "https://github.com/actions/runner"},


"buildType": "https://github.com/actions/workflow",


"materials": [{


"uri": "git+https://github.com/org/repo",


"digest": {"sha1": "def..."}


}],


"buildConfig": {


"steps": [{


"command": "docker build -t myapp .",


"env": {}


}]


}


}


)





Verification Policies




# signing-policy.yaml


verification_policy:


container_images:


- require_signature: true


- allowed_signers:


- identity: "*.github.com"


issuer: "https://token.actions.githubusercontent.com"


- require_attestations:


- slsa_provenance


- vulnerability_scan




packages:


- require_pgp_signature: true


- trusted_keys:


- fingerprint: "ABCD1234..."


- fingerprint: "EFGH5678..."




git_commits:


- require_signed_commits: true


- minimum_key_strength: 2048





Conclusion

Software signing is fundamental to supply chain security. GPG works well for traditional signing, but Sigstore and cosign offer easier keyless signing workflows. Use in-toto attestations for end-to-end provenance. Enforce signing policies in your CI/CD pipeline. Verify signatures at every consumption point: container registries, package managers, and deployment pipelines.