Signing and Verification

OCM uses cryptographic signatures to guarantee that component versions are authentic (created by a trusted party) and have not been tampered with during storage or transfer.

Why Sign Components?

The software lifecycle involves multiple stages: development, build, packaging, distribution, and deployment. At each stage, components could potentially be:

  • Modified — malicious actors could inject code or alter resources
  • Replaced — components could be swapped for compromised versions
  • Misattributed — components could falsely claim to come from a trusted source

Signing addresses these risks by creating a cryptographic proof of:

  1. Integrity: The component has not changed since it was signed
  2. Authenticity: The signature was created by someone with access to the private key
  3. Provenance: The signer cannot deny having signed the component

How OCM Signing Works

  flowchart TB
    subgraph sign ["Sign (Producer)"]
        direction TB
        A[Component Version] --> B[Normalize & Hash]
        B --> C[Sign with Private Key]
        C --> D["Signature embedded in CV"]
    end
    
    sign --> T["Transfer Component Version"]
    
    T --> verify
    
    subgraph verify ["Verify (Consumer)"]
        direction TB
        E[Component Version] --> F[Extract Signature]
        E --> G[Normalize & Hash]
        F --> H[Verify with Public Key]
        G --> H
        H --> I{Valid?}
        I -->|Yes| VALID["✓ Trusted"]
        I -->|No| INVALID["✗ Rejected"]
    end

Normalization and Digest Calculation

OCM uses a two-layer approach to ensure consistent and reproducible digests:

Component Descriptor Normalization

Before hashing, the component descriptor is normalized into a canonical form, eliminating any ambiguities that could cause the same logical descriptor to produce different digests. The default normalization algorithm (jsonNormalisation/v4alpha1) defines exactly how this canonical form is derived, ensuring identical component descriptors always yield the same digest.

This entire approach relies on the fact that content digests are preserved across transfers. Each resource’s digest is computed from its actual content (not from where it is stored), and this digest is recorded in the component descriptor. When a component version is transported to a different registry, the access references change, but the content — and therefore its digest — remains the same. This is why the signature remains valid after transfer.

Artifact Digest Normalization

OCM supports different digest algorithms for different artifact types. The algorithm determines how a resource’s content is hashed to produce its digest:

AlgorithmDescription
genericBlobDigest/v1Direct hash of ordinary blob bytes, such as an HTTP chart archive. Historical OCI descriptors may use this label for a manifest hash.
ociArtifactDigest/v1Digest of the selected OCI manifest or index, used for newly created OCI resources.

The two normalisations are not interchangeable: their names also contribute to the signed descriptor. See OCI Resource Digests for verification and compatibility rules.

Recursive Component References

When a component version is signed, the digests of referenced components are calculated and embedded into the component descriptor. This does not happen automatically when references are created — without signing, references can exist without digests, and in that case there is no integrity guarantee for the referenced components.

references:
  - componentName: ocm.software/helper
    name: helper
    version: 1.0.0
    digest:
      hashAlgorithm: SHA-256
      normalisationAlgorithm: jsonNormalisation/v4alpha1
      value: 01c211f5c9cfd7c40e5b84d66a2fb7d19cb0...

Once signed, this creates a complete integrity chain — verifying the root component automatically verifies all transitive dependencies.

What Gets Signed?

OCM signs a digest of the normalized component descriptor (see Normalization and Digest Calculation for how the canonical form is derived). The signed digest covers:

  • Component metadata (name, version, provider)
  • Resource descriptors (including digest, if available)
  • Source descriptors (including digest, if available)
  • Component references (including digest, if available)
  • Labels marked with signing: true (at any level)

Labels without signing: true are excluded from the digest and do not affect the signature. Storage-related fields like access and repositoryContexts are also excluded — see Normalization and Digest Calculation above.

The signature does not cover the raw resource content directly — instead, it covers the digests of those resources as recorded in the component descriptor. The access field (which describes where a resource is stored) is excluded from the signed digest. This is a key design principle:

  • Location-independent integrity — a component version can be transferred to a different registry (changing all access references) without invalidating its signature. The digest remains stable because it depends only on what the artifacts contain, not where they are stored.
  • Any change to resource content changes its digest, invalidating the signature.
  • Signature verification is fast (no need to re-hash large binaries).

This separation of content identity from storage location is what enables secure delivery across environments: a producer signs a component version once, and consumers can verify it after any number of transfers — even into air-gapped environments with completely different registries.

The following example shows a signed component descriptor. Notice that each resource has both an access field (storage location) and a digest field (content hash). Only the digest is included in the signature — the access can change freely during transfers:

Example Signed Component Descriptor
component:
  name: github.com/acme.org/helloworld
  version: 1.0.0
  provider: acme.org
  resources:
    - name: mylocalfile
      type: blob
      version: 1.0.0
      relation: local
      access:                          # NOT included in signature
        type: LocalBlob/v1
        localReference: sha256:70a257...
        mediaType: text/plain; charset=utf-8
      digest:                          # Included in signature
        hashAlgorithm: SHA-256
        normalisationAlgorithm: genericBlobDigest/v1
        value: 70a2577d7b649574cbbba99a2f2ebdf27904a4abf80c9729923ee67ea8d2d9d8
    - name: image
      type: ociImage
      version: 1.0.0
      relation: external
      access:                          # NOT included in signature
        type: OCIImage/v1
        imageReference: ghcr.io/stefanprodan/podinfo:6.9.1@sha256:262578cd...
      digest:                          # Included in signature
        hashAlgorithm: SHA-256
        normalisationAlgorithm: ociArtifactDigest/v1
        value: 262578cde928d5c9eba3bce079976444f624c13ed0afb741d90d5423877496cb
signatures:
  - name: default
    digest:
      hashAlgorithm: SHA-256
      normalisationAlgorithm: jsonNormalisation/v4alpha1
      value: 91dd197868907487e62872695db1fa7b397fde300bcbae23e24abc188fb147ad
    signature:
      algorithm: RSASSA-PSS
      mediaType: application/vnd.ocm.signature.rsa.pss
      value: 7feb449229c6ffe368144995432befd1505d2d29...

Signature Storage

Signatures are stored as part of the component version:

signatures:
  - name: acme-release-signing
    digest:
      hashAlgorithm: SHA-256
      normalisationAlgorithm: jsonNormalisation/v4alpha1
      value: abc123...
    signature:
      algorithm: RSASSA-PSS
      mediaType: application/vnd.ocm.signature.rsa
      value: <base64-encoded-signature>

A component version can have multiple signatures from different parties, enabling:

  • Separation of build and release signing
  • Multiple approval workflows
  • Cross-organizational trust chains

Pinning a Component Version by Digest

A component reference can name the digest of the component version it expects. The digest uses the OCI digest notation, optionally prefixed with a normalization algorithm (/ written as .):

ghcr.io/acme//acme.org/helloworld@sha256:<oci-digest>
ghcr.io/acme//acme.org/helloworld:1.0.0@sha256:<oci-digest>
ghcr.io/acme//acme.org/helloworld:1.0.0@jsonNormalisation.v4alpha1+sha256:<normalized-descriptor-digest>

ocm get, ocm download, ocm verify, ocm sign and ocm transfer fail if the component version they read does not match the pin, and ocm download verifies the downloaded content against the resource digests of the pinned descriptor. Both forms accept sha256 and sha512.

OCI digest sha256:<hex>Normalized pin jsonNormalisation.v4alpha1+sha256:<hex>
Identifiesthe OCI image index or manifest the component version is stored as, which is what its version tag resolves tothe descriptor normalized with jsonNormalisation/v4alpha1 (see Normalization and Digest Calculation)
Coversthe stored artifact graph: descriptor (including access information, all labels and signatures) and all local blobswhat signatures cover: no access information, no labels excluded from signing
How it is checkedthe component version is read from the artifact with that digest, never through the version tag; every node read is verified against its digestthe descriptor read through the version tag is normalized and hashed
Version in the referenceoptional; if given, the artifact must hold that versionrequired
Changes onany change of the stored artifact, including ocm transfer and ocm signchanges of signed content only
Where it worksthe repository the pin was taken from; OCI registries and CTF archives onlyevery repository the component version is transferred to
How to obtainocm verify cv in an OCI registry or CTF archive (reference in SIGNATURE VERIFICATION SUCCESSFUL); any OCI tool, for example oras resolve ghcr.io/acme/component-descriptors/acme.org/helloworld:1.0.0 or crane digest; in a CTF archive, the digest of the version tag in artifact-index.jsonthe digest ocm sign logs (digest in signed successfully) and records in the signature (signatures[].digest.value in ocm get cv -o yaml)

An OCI digest pins a component version the way a digest pins a container image: because the digest of the image index or manifest covers everything stored below it, reading by digest yields exactly the content that was pinned, even if the version tag has been moved since. ocm transfer with an OCI digest pin copies the descriptor and all local resources from the pinned artifact. Use it to bind a pipeline step to the exact component version an earlier step inspected in one repository. Use a normalized pin to identify a component version independent of where it is stored: it survives transfer and signing, so a pin taken once binds every later read to the same signed content. For repositories that do not store component versions as OCI artifacts, use the normalized form. See the CLI reference for the reference format.

ocm verify cv closes the gap between verifying a component version and using it: in an OCI registry or CTF archive it resolves the version tag once, verifies the artifact the tag resolved to, and logs the reference pinned to that artifact’s OCI digest. Passing that reference to ocm transfer or ocm download reads exactly the verified content, even if the tag is moved in between. ocm sign with an OCI digest pin writes the signature to the version tag, so it refuses to sign if the tag no longer resolves to the pinned artifact instead of replacing the current version with the pinned one.

Supported Signing Algorithms

OCM’s signature.algorithm field selects between two signing approaches: classical RSA signatures over a long-lived key pair, and Sigstore-based keyless signing, where each signature is made with a fresh, short-lived key bound to your OIDC identity. The two approaches differ not just in cryptography but in their trust model — see Trust Models below.

AlgorithmTypeTrust ModelCharacteristics
RSASSA-PSS (default)Asymmetric (RSA)Public key or certificate chainProbabilistic, stronger security guarantees, recommended for new RSA-based implementations
RSA-PKCS#1 v1.5Asymmetric (RSA)Public key or certificate chainDeterministic, widely supported, compatible with legacy systems
Sigstore (keyless, early access)Asymmetric (ECDSA, ephemeral)OIDC identityShort-lived certificate from Fulcio bound to your OIDC identity, transparency-log entry in Rekor; no long-lived keys to manage

To override the default signing algorithm or encoding policy, add a signing.config.ocm.software/v1alpha1 entry to your .ocmconfig and put the signer under its signer field. See the CLI reference. The signer configures only the algorithm and encoding policy; credentials are always resolved separately from the credentials configuration. Give the entry a signature field to apply it to that one signature only, matching the --signature flag; without it the entry applies to every signature. The same entry carries the verification side under its verifier field, read by ocm verify (see the CLI reference). Because the verifier is resolved per signature, a component version carrying several signatures is verified with the handler that each signature name resolves to.

For RSA-based signing, OCM uses PEM-encoded key files configured in the .ocmconfig:

  • Private keys: Used by producers to sign component versions
  • Public keys: Distributed to consumers for verification

See How-to: Generate Signing Keys for creating RSA key pairs.

Sigstore-based signing has no long-lived keys: a fresh signing key is generated per signature and certified by Fulcio against your OIDC identity. See Sigstore (Keyless) below.

Signature Encoding Policies

The signatureEncodingPolicy in the signer spec controls how the signature output is serialized and stored. It does not affect the format of key input files, which are always PEM-encoded.

PolicySignature FormatMedia TypeCertificate ChainVerification Requires
Plain (default)Hex-encoded raw bytesapplication/vnd.ocm.signature.rsa.pssNot embeddedExternally supplied public key
PEM (early access)PEM SIGNATURE block + CERTIFICATE blocksapplication/x-pem-fileEmbedded in signatureValid certificate chain in signature

Plain Encoding (Default)

The raw RSA signature bytes are hex-encoded and stored directly. This is the most compact representation. Verification always requires the public key to be provided separately via .ocmconfig credentials.

Example signature in a component descriptor:

signature:
  algorithm: RSASSA-PSS
  mediaType: application/vnd.ocm.signature.rsa.pss
  value: d1ea6e0cd850c8dbd0d20cd39b9c7954...

PEM Encoding (Early Access)

The signature is wrapped in a PEM block of type SIGNATURE, optionally followed by the signer’s X.509 certificate chain. This makes the signature self-contained: verifiers can extract and validate the public key from the embedded chain without needing a separately distributed key.

Example of a PEM-encoded signature value:

signature:
  algorithm: RSASSA-PSS
  mediaType: application/x-pem-file
  value: |
    -----BEGIN SIGNATURE-----
    Signature Algorithm: RSASSA-PSS
    
    <base64-encoded signature bytes>
    -----END SIGNATURE-----
    -----BEGIN CERTIFICATE-----
    <leaf certificate>
    -----END CERTIFICATE-----
    -----BEGIN CERTIFICATE-----
    <intermediate CA, if applicable>
    -----END CERTIFICATE-----
PEM encoding is in early access

PEM encoding is currently being rolled out across projects and we are awaiting feedback. The interface may evolve based on that feedback.

Key files vs. signature encoding

A common source of confusion: “PEM” in signatureEncodingPolicy refers to the signature output format, not the key input format. Input keys are always PEM-encoded files (e.g. -----BEGIN RSA PRIVATE KEY-----), regardless of which encoding policy is selected.

When using PEM encoding for signing, the credential referenced by public_key_pem / public_key_pem_file must contain X.509 certificates (not bare public keys), because the certificate chain is embedded into the signature for self-contained verification.

Sigstore (Keyless)

Sigstore replaces the long-lived key pair at the heart of RSA signing with a short-lived certificate bound to your OIDC identity. The signer logs in to an identity provider; Sigstore issues a certificate valid for ~10 minutes; the signature is recorded in a public transparency log. Verifiers don’t pin a public key — they declare which identity they trust.

The Sigstore stack is four cooperating pieces. For the public-good happy path (sigstore.dev) you don’t deploy any of them; for an enterprise stack a platform team has already deployed them and you point your config at their endpoints.

  • OIDC IdP — the identity provider you log in to (Google, GitHub, Microsoft, or your corporate IdP). The token it issues proves who is signing.
  • Fulcio — a short-lived certificate authority. It accepts your OIDC token and issues a certificate (valid for ~10 minutes) that binds the token’s identity claims to a fresh signing key.
  • Rekor — an append-only public transparency log. Every signature is recorded so anyone can audit when, and by whom, it was made.
  • TUF — the mechanism clients use to discover the current trusted roots for Fulcio and Rekor. The verifier uses it to know which CA and which log to trust.

The signature payload stored in the component descriptor is a Sigstore bundle: the signature bytes, the Fulcio certificate, and the Rekor inclusion proof, all in one self-contained blob. This self-containment is a key design principle of Sigstore: the signature value includes everything a verifier needs to validate the signature and establish trust in the signer’s identity, without needing any out-of-band information.

This matters directly for OCM’s sovereign-delivery and air-gapped scenarios. The component version is the unit of transport, and OCM carries the proof of authorship along with it: the signed descriptor and the bundle embedded inside it travel as one.

The only piece a verifier needs in addition is a local trusted-root file — the public keys of the Fulcio CA and the Rekor instance the component was signed against, whether that is public-good Sigstore or an enterprise stack — distributed into the disconnected environment once, out of band.

With those pieces in place, ocm verify cv runs entirely offline: no callback to any Sigstore service, no TUF refresh, no network egress whatsoever. This is what makes Sigstore viable in regulated and sovereign-cloud deployments where egress to public or on-premise infrastructure is not permitted at verification time.

signature:
  algorithm: Sigstore/v1alpha1
  mediaType: application/vnd.dev.sigstore.bundle.v0.3+json
  value: <base64-encoded Sigstore bundle>

Because the certificate is in the bundle and the trust roots come from TUF (or an enterprise trusted-root file), no encoding-policy choice is involved — Sigstore is its own algorithm with its own bundle format.

For how this changes what verifiers pin, see Identity-Based Trust below.

Sigstore signing is in early access

Sigstore (keyless) signing is currently being rolled out and we are awaiting feedback. The interface may evolve based on that feedback.

Trust Models

The signing approach you choose determines how verifiers establish trust in a signature. RSA-based algorithms support two models depending on encoding policy (key pinning or certificate chain); Sigstore brings a third, identity-based model. OCM supports three trust models in total.

Key Pinning (Plain Encoding)

The verifier explicitly configures the signer’s public key in .ocmconfig. Trust is established by knowing the exact key that was used to sign.

  • No PKI infrastructure required
  • Simple to set up for small teams or self-signed workflows
  • Verifier must obtain the public key out-of-band (e.g., from a secrets manager or shared repository)
  • Rotating keys requires updating every verifier’s configuration

Certificate Chain Trust (PEM Encoding)

The signer embeds the certificate chain (leaf + any intermediates) directly in the signature value. The verifier pins only the root CA certificate as a trust anchor.

  • Requires PKI infrastructure (or a locally generated CA)
  • Verifier only needs the root CA – leaf certificates can change without reconfiguring verifiers
  • Supports organizational delegation: the root CA can issue intermediate CAs for different teams
  • The root CA is never embedded in the signature; OCM rejects self-signed certificates found in the embedded chain

Identity-Based Trust (Sigstore)

The signer authenticates to an OIDC identity provider; Fulcio binds that identity to a short-lived certificate; Rekor records the signature in a public transparency log. The verifier doesn’t pin a key or a CA — it declares which OIDC identity it trusts.

  • No long-lived keys to manage, distribute, or rotate
  • Trust anchor is the signer’s identity (e.g. jane.doe@example.com via https://github.com/login/oauth), not a public key
  • Public-good Sigstore (sigstore.dev) is shared infrastructure; an enterprise stack runs Fulcio/Rekor/TUF inside the organization
  • Audit trail is automatic and public (or organization-internal for enterprise stacks)

When to Use Each

CriterionPlain (Key Pinning)PEM (Certificate Chain)Sigstore (Identity)
Long-lived keys to manageYesYes (issued by your CA)No
PKI infrastructure availableNoYesN/A (Fulcio replaces it)
Number of signersFewMany or changingAnyone with an OIDC identity
Key rotation complexityHigh (update all verifiers)Low (root CA stays stable)None (certs are short-lived)
Signature is self-containedNo (public key needed separately)YesYes (bundle includes cert + log proof)
Audit trailBuild your ownBuild your ownBuilt-in (Rekor)
Recommended forSimple setups, personal projectsEnterprise environments with existing PKITeams that want to skip key management entirely

For hands-on steps, see Tutorial: Plain Signatures and Tutorial: Certificate Chains (PEM).

RFC 3161 Timestamping

TSA timestamping is in early access

RFC 3161 timestamping is currently being rolled out and we are awaiting feedback. The interface may evolve based on that feedback.

A signature verifies only while the signing certificate is valid. With certificate-chain trust (PEM encoding), a signature can become unverifiable once the certificate expires — even though it was valid when created. RFC 3161 timestamping solves this: at signing time a trusted Timestamping Authority (TSA) counter-signs the signature, proving when it was made. At verification time — for RSA/PEM certificate-chain signatures — the certificate chain is then validated against the TSA-attested time instead of the current time, so a signature created while the certificate was valid keeps verifying after it expires. This certificate-expiry extension applies only when the timestamp is trusted (its chain validates against TSA roots the verifier configured). Plain signatures carry no certificate, so they gain no post-expiry verification from a timestamp. Sigstore signatures are timestamped by Sigstore itself, with the TSAs from its signing config; --tsa/--tsa-url are rejected for Sigstore signers.

Component versions signed and timestamped with the OCM v1 CLI keep verifying: their timestamps (PEM block TIMESTAMP INFO, taken over the descriptor digest) are checked the same way. Because the descriptor digest is known before signing, such a timestamp does not prove when the signature was made, so its time is only used to validate an expired certificate with ocm verify cv --insecure-trust-legacy-timestamps. Timestamps created by this CLI use a newer format that OCM v1 cannot read.

Timestamping is optional. A timestamp can be attached to Plain and PEM signatures alike, but only PEM certificate-chain (RSA) signatures benefit from the certificate-expiry extension. Signatures without a timestamp continue to verify unchanged.

How It Works

  flowchart TB
    subgraph sign ["Sign"]
        A[Compute digest] --> B[Sign digest]
        B --> C[Request timestamp of signature from TSA]
        C --> D["Attach timestamp token to signature"]
    end
    sign --> V
    subgraph V ["Verify"]
        E[Resolve TSA roots from credential graph] --> F[Verify timestamp token chain at GenTime]
        F --> G["If trusted, validate signing cert at TSA-attested time"]
    end
  1. The signer selects a TSA with the --tsa (default public TSA) or --tsa-url flag on ocm sign cv.
  2. After signing, OCM sends a hash of the signature value to the TSA, which returns a PKCS#7 timestamp token whose signer certificate must carry a critical id-kp-timeStamping EKU. The token is attached to the signature’s timestamp field in the descriptor, and the TSA URL is recorded in the label url.tsa.ocm.software/{signatureName}. The label is not signing-relevant, so the component digest stays the same and references to the component as well as other signatures remain valid.
  3. At verification, OCM reads the TSA URL from that label and resolves the TSA’s root CA certificates from the credential graph via a TSA/v1alpha1 consumer identity.
  4. The token’s PKCS#7 chain is verified against those roots at its GenTime, its message imprint is checked against a hash of the signature value, and — only when the chain is trusted — the attested time is used to validate an RSA/PEM signing certificate chain. Without trusted roots the token is checked structurally only and does not relax certificate validity. Only built-in signing handlers use the attested time; external signing plugins validate certificates at the current time.

Trust Model

Timestamping keeps three trust domains separate: the signer proves who signed, the TSA proves when, and the verifier independently decides what to trust. Critically, the TSA root certificates come exclusively from the verifier’s credential graph — never from the signature — so a signer cannot embed a self-signed TSA root and have it trusted. The TSA URL label is not signed; it only selects among the roots the verifier configured. A token is trusted only when it chains to one of them, so pointing the URL at a TSA an attacker controls does not get their timestamp accepted.

Verification degrades gracefully across three levels:

LevelTSA roots configured?Timestamp present?What is verified
No timestampN/ANoStandard signature verification only
StructuralNoYesPKCS#7 parsing + imprint match; no chain verification; a warning is logged
FullYesYesPKCS#7 chain verification + imprint match + TSA time used for certificate validity

Configuring TSA Trust

TSA root certificates are configured on the verifier side through the credential graph, using the typed TSACredentials/v1alpha1 credential:

type: generic.config.ocm.software/v1
configurations:
  - type: credentials.config.ocm.software
    consumers:
      - identity:
          type: TSA/v1alpha1
          hostname: timestamp.digicert.com
          scheme: http
        credentials:
          - type: TSACredentials/v1alpha1
            rootCertsPEMFile: /path/to/digicert-tsa-root.pem

The signing side needs no TSA credentials; the TSA is chosen with the CLI flags. For the full attribute and field reference, see Consumer Identities → TSA/v1alpha1 and Credential Types → TSACredentials/v1alpha1.

Two certificates, two mechanisms

Two distinct certificates are easy to conflate — especially with a self-hosted TSA, where both are often issued by the same internal CA:

CertificateValidated whenTrusted via
The TSA’s token-signing cert (PKCS#7 chain over the timestamp token)VerifyingTSACredentials/v1alpha1 rootCertsPEM
The TSA server’s HTTPS serving cert (the TLS connection when requesting a timestamp)SigningHTTP client config rootCAsPEM (or insecureSkipVerify for dev)

TSACredentials roots verify the token, not the connection. To make the signer trust a private TSA’s HTTPS serving certificate, add its CA to the HTTP client configuration (http.config.ocm.software, field rootCAsPEM / rootCAsPEMFile, per-host capable), which the signing-time TSA request now honours. For a throwaway self-signed dev TSA, insecureSkipVerify: true scoped to that host disables connection verification without affecting token trust.

For the complete trust analysis and design rationale, see ADR 0032 (RFC 3161 Timestamping) in the repository’s docs/adr/ directory.

Next Steps