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.