Skip to content

Corporate PKI (X.509)

When compliance mandates identity-traceable signing, sign with a leaf certificate that chains to the corporate CA. The signature carries the certificate, so verification is attributable to a named identity and chains to the CA — still fully offline (no Fulcio, no Rekor).

The signing identity

The signing certificate is issued by the corporate CA (or by gapcert / step-ca against it) and carries:

  • a SAN identity (email / URI / DNS), e.g. gapsign-ci@corp.example;
  • the Sigstore OIDC-issuer extension (v1 …57264.1.1 or v2 …57264.1.8), e.g. https://corp.example/oidc;
  • the code-signing extended key usage.

Sign

# config.yaml (signing side)
backend: cosign-x509
backend_opts:
  key:   ~/.config/gapsign/leaf.key      # leaf private key
  cert:  ~/.config/gapsign/leaf.crt       # leaf certificate
  chain: ~/.config/gapsign/corp-ca.pem    # CA chain
$ gapsign sign -config config.yaml registry.internal/app:v1

The signature is stored with the certificate and chain in the cosign annotations (dev.sigstore.cosign/certificate, dev.sigstore.cosign/chain).

Verify

gapsign verifies: the signature under the leaf certificate's key, the digest+repository binding, that the leaf chains to the configured CA roots as a code-signing certificate, and that the SAN identity and issuer match what policy expects. Trust roots come only from your configuration — never from the signature — so an attacker's self-signed leaf or CA in the signature can never become a trust anchor.

The real cosign CLI verifies the same artifact offline:

$ cosign verify --cert-chain corp-ca.pem \
    --certificate-identity gapsign-ci@corp.example \
    --certificate-oidc-issuer https://corp.example/oidc \
    --insecure-ignore-tlog --insecure-ignore-sct \
    registry.internal/app@sha256:...

Admission

For an X.509 signer, the config trust: entry is the CA + identity + issuer:

trust:
  mirror-x509:
    ca_roots:  ~/.config/gapsign/corp-ca.pem
    identity:  "gapsign-ci@corp.example"
    issuer:    "https://corp.example/oidc"

gapsign policy render emits a Kyverno keyless attestor (CA roots + exact identity + issuer, offline). The ca_roots file must contain only certificates — a mis-pointed private key is refused before anything is written.

The gapcert linkage

The two airgap-infra siblings compose: gapcert (ACME / step-ca against the corporate CA) can issue the X.509 signing identity, and gapsign signs with it — one gets the identity, the other signs with it.