Product Update Manifest Contract v1

Contract: stella.ops/productUpdateManifest@v1 (constant ProductUpdateManifestContract.SchemaVersion). Predicate type: stella.ops/product-update-manifest@v1 (PredicateTypes.StellaOpsProductUpdateManifest; also returned from PredicateTypes.GetAllowedPredicateTypes()). Manifest media type: application/vnd.stellaops.product-update-manifest.v1+json (ProductUpdateManifestContract.ManifestPayloadType). DSSE in-toto media type: application/vnd.in-toto+json (ProductUpdateManifestContract.InTotoPayloadType). This is the payloadType the DSSE envelope carries. Status: Active Signer contract; release CI publication remains blocked until the signed SBOM archive dependency and release workflow wiring are complete. Sprint: SPRINT_20260430_004 Owner: Attestor/Signer guild + Stella manufacturer of record Implementation: src/Attestor/StellaOps.Signer/StellaOps.Signer.Core/ProductUpdateManifest.cs (ProductUpdateManifestContract, the ProductUpdateManifest record graph, ProductUpdateManifestBuilder, and ProductUpdateManifestOfflineVerifier). Predicate-type constants live in PredicateTypes.cs. The EU signing-registry entry is defined in EuSigningContracts.cs (EuSigningPayloadRegistry.ProductUpdateManifest). Schema-pinning policy: Pinned per Stella minor release. Consumers MUST require the exact schemaVersion they support and MUST NOT auto-follow newer manifest schemas. Deterministic fixture location: src/Attestor/StellaOps.Signer/StellaOps.Signer.Tests/Signing/ProductUpdateManifestTests.cs. Replay/offline expectations: Manifest canonicalization, DSSE signature verification, and semantic subject-to-predicate checks MUST run without network access. EU signing registry: product-update-manifest-v1 in docs/contracts/eu-signing-payload-registry-v1.md (registry id EuSigningPayloadRegistry.ProductUpdateManifest; registered with PayloadShape = InTotoStatement, Audience = ProductManufacturer, ExpectedSignerProfile = stella-manufacturer-release, schema-pin JSON path schemaVersion).


Purpose

product-update-manifest-v1 is the signed release manifest for Stella Ops binaries and container images, and the authoritative input for CRA Annex I update-channel integrity. It is the contract for the Signer pipeline that produces it, the Router gateway that publishes its metadata, and the stella verify release-manifest CLI that verifies it offline.

The manifest binds:

The manifest is signed as an in-toto statement through the existing Signer pipeline. The manifest body does not contain its own mutable signature field; the signature is the surrounding DSSE envelope.


Canonicalization And Signing

ProductUpdateManifestBuilder.Normalize performs the deterministic normalization (it also runs Validate first and throws SignerValidationException on any failure):

  1. Trim release, version, craApplicability, releaseNotesUrl, and the string fields of each image and advisory.
  2. Normalize image entries: trim name/platform/sbomRef, lowercase the digest, then order by name, then platform, then digest using ordinal ordering.
  3. Normalize fixed advisories: order by source, then id using ordinal ordering. Within each advisory, cveIds are trimmed, upper-cased, empty entries dropped, de-duplicated, and ordinal-sorted.
  4. Normalize SHA-256 digests to lowercase sha256:<64 hex> (NormalizeSha256Digest).
  5. Canonicalize the normalized manifest JSON with the repo canonical JSON helper (CanonJson.Canonicalize) via ProductUpdateManifestBuilder.SerializeCanonical.
  6. Compute manifestSha256 = CanonJson.Sha256Prefixed(canonicalManifestBytes) via ProductUpdateManifestBuilder.ComputeSha256 (returns the sha256:<hex> form).
  7. ProductUpdateManifestBuilder.BuildSigningRequest builds the in-toto subjects. SignerStatementBuilder.BuildStatementPayload then emits the statement with:
    • _type: https://in-toto.io/Statement/v0.1 (the Signer default emitted by BuildStatementPayload(request); GetRecommendedStatementType recommends https://in-toto.io/Statement/v1 for StellaOps predicate types, but the product-update-manifest path uses the default).
    • predicateType: stella.ops/product-update-manifest@v1.
    • predicate: canonical manifest JSON.
    • subject[0]: stella-release-manifest:<release>:<version> with digest { "sha256": <manifestSha256 hex without prefix> }.
    • Additional subjects for each image, using the image name and the image sha256 digest hex.
  8. Sign the statement as a DSSE envelope with payloadType = application/vnd.in-toto+json.

BuildSigningRequest also requires a scannerImageDigest, a ProofOfEntitlement, and SigningOptions — these flow through the standard Signer pipeline and are not part of the manifest body.

Re-signing with the same manifest inputs, signing key, and Signer options MUST produce the same statement payload hash. Signature bytes may vary by provider when the configured algorithm is randomized, but the manifest hash and statement payload hash must remain stable.


JSON Shape

{
  "schemaVersion": "stella.ops/productUpdateManifest@v1",
  "release": "stella-ops",
  "version": "2026.9.0",
  "placedOnMarketAt": null,
  "madeAvailableInEu": "2026-09-01",
  "craApplicability": "pre-cra",
  "substantialModificationOf": null,
  "supportPolicy": {
    "lifecycleSchema": "stella.ops/productLifecycle@v1",
    "releaseTrain": {
      "version": "2026.9",
      "releasedAt": "2026-09-01",
      "securitySupportUntil": "2031-09-01",
      "eolAt": "2031-09-01",
      "lts": false
    }
  },
  "images": [
    {
      "name": "registry.stella-ops.org/stellaops/signer",
      "platform": "linux/amd64",
      "digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
      "sbomRef": "oci://registry.stella-ops.org/stellaops/signer:2026.9.0-sbom"
    }
  ],
  "releaseNotesUrl": "https://stella-ops.org/releases/2026.9.0",
  "fixesAdvisories": [
    {
      "id": "STELLA-2026-0001",
      "source": "stella-product-csaf",
      "cveIds": ["CVE-2026-0001"],
      "summary": "Fixes signed update metadata validation.",
      "advisoryUrl": "https://stella-ops.org/security/advisories/STELLA-2026-0001.json"
    }
  ]
}

Field Definitions

FieldTypeRequiredDescription
schemaVersionstringyesAlways stella.ops/productUpdateManifest@v1. Validated for exact equality.
releasestringyesProduct release family, normally stella-ops. Validated non-blank.
versionstringyesReleased Stella version. Validated non-blank.
placedOnMarketAtstring date or nullyes (key present; value MAY be null)CRA full-placement date. null for pre-full-CRA releases. The manifest validator does not parse or constrain the date value.
madeAvailableInEustring date or nullyes (key present; value MAY be null)Date the release became available to EU users. The manifest validator does not parse or constrain the date value. (The Gateway product-metadata publisher, however, requires this to be a non-empty string.)
craApplicabilityenumyesOne of pre-cra, cra-reporting-only, cra-full. Validated against this exact set.
substantialModificationOfstring or nullyes (key present; value MAY be null)Predecessor release if the manifest records a substantial modification. Not validated.
supportPolicyobjectyesLifecycle block from product-lifecycle-v1. Validated non-null.
imagesarrayyesNon-empty image list. Each image must be digest-pinned.
releaseNotesUrlstringyesRelease notes URL or offline-kit relative URI. Validated non-blank.
fixesAdvisoriesarrayyes (key present; MAY be empty)Fixed-advisory summaries; empty array is allowed for non-security releases.

Property names are emitted exactly as shown (the serializer uses PropertyNamingPolicy = null, so the [JsonPropertyName] camelCase names on the ProductUpdateManifest record graph are authoritative).

supportPolicy

FieldTypeRequiredDescription
lifecycleSchemastringyesAlways stella.ops/productLifecycle@v1 for this contract (ProductUpdateManifestContract.LifecycleSchemaVersion). Validated for exact equality.
releaseTrainobjectyesLifecycle record copied from the pinned lifecycle source. Validated non-null. Its version, releasedAt, securitySupportUntil, and eolAt are each validated non-blank; lts is a boolean.

images[]

FieldTypeRequiredDescription
namestringyesOCI repository or release artifact name.
platformstringyesPlatform string such as linux/amd64.
digeststringyesLowercase sha256:<64 hex> digest.
sbomRefstringyesOCI, CAS, or offline-kit reference to the signed SBOM archive for the image.

fixesAdvisories[]

FieldTypeRequiredDescription
idstringyesStella advisory or feed item identifier. Validated non-blank.
sourcestringyesAdvisory source, for example stella-product-csaf. Validated non-blank.
cveIdsarrayyes (MUST be present, non-null; MAY be empty)CVE IDs. During normalization they are trimmed, upper-cased, empty entries dropped, de-duplicated, and ordinal-sorted. Empty array is allowed when the fix is not CVE-backed. The validator does not enforce CVE-ID syntax.
summarystringyesShort summary of the fixed issue (trimmed during normalization). Validated non-blank.
advisoryUrlstringyesOnline URL or offline-kit relative path to the advisory. Validated non-blank.

Validation Rules

ProductUpdateManifestBuilder.Validate (invoked from Normalize) enforces the following, throwing SignerValidationException with the listed code on the first failure:

The signed statement and offline verifier additionally require:

Rules enforced outside the manifest validator (downstream):

Not implemented as stated: craApplicability is not cross-validated against placedOnMarketAt / madeAvailableInEu (no such check exists in ProductUpdateManifestBuilder.Validate; craApplicability is only checked against the fixed enum set, and the date fields are not parsed). Treat the placement-anchor alignment as an authoring/process expectation, not an automated invariant.


Offline Verification

The Signer library implementation is ProductUpdateManifestOfflineVerifier.VerifyAsync, which takes a ProductUpdateManifestOfflineVerificationRequest(EnvelopeJson, TrustedPublicKeysPem, TrustRootPaths) and returns a ProductUpdateManifestOfflineVerificationResult(IsValid, Manifest, ManifestSha256, PayloadSha256, VerifiedKeyIds, Issues). It performs the following, all without network access:

  1. Parse the DSSE envelope JSON (payloadType, payload, signatures[]). Signature entries accept either keyid/sig or keyId/signature property spellings. Failure -> envelope_parse_error:<msg> / payload_type_missing / payload_missing / signatures_missing.
  2. Require payloadType == application/vnd.in-toto+json; otherwise fail with payload_type_invalid.
  3. Load trust roots from inline PEM strings (TrustedPublicKeysPem) and from TrustRootPaths (each path may be a PEM file or a directory recursively scanned for *.pem). PEM may be a PUBLIC KEY block or a CERTIFICATE (the EC/RSA public key is extracted). Private-key PEM blocks are rejected. If no usable trust roots resolve, fail with trust_roots_required.
  4. Base64-decode the payload (standard or base64url accepted); failure -> payload_invalid_base64.
  5. Verify each DSSE signature over the PAE (DssePreAuthenticationEncoding.Compute(payloadType, payloadBytes)) against every trust root. Supported algorithms: ECDSA with SHA-256/384/512 and RSA (PKCS1 SHA-256, PSS SHA-256/384/512). A matched signature records its keyid in VerifiedKeyIds; unmatched signatures add signature_invalid:<keyid> (or signature_invalid_base64:<keyid>). If zero signatures verify, the result is invalid.
  6. Parse the in-toto statement payload and require a non-empty _type (statement_type_missing) and predicateType == stella.ops/product-update-manifest@v1 (predicate_type_invalid); require a predicate (predicate_missing).
  7. Deserialize and re-normalize the predicate into a ProductUpdateManifest, recompute ManifestSha256 via ProductUpdateManifestBuilder.ComputeSha256, and validate the statement subject[] array contains the manifest subject (stella-release-manifest:<release>:<version>) with the matching sha256 digest and an image subject per image (manifest_subject_digest_mismatch, image_subject_digest_missing:<name>).
  8. IsValid is true only when both signature verification and semantic checks produce no issues. The verifier fails closed on missing trust roots, malformed payloads, invalid signatures, digest mismatches, and unsupported schema/payload types.

Note: PayloadSha256 is the SHA-256 of the raw signed payload bytes; ManifestSha256 is the canonical hash of the re-normalized predicate. These are distinct values.

A separate generic verifier, EuArtifactOfflineVerifier.VerifyAsync (EuSigningContracts.cs), can verify this artifact via its registry entry (product-update-manifest-v1); it additionally enforces the schema-pin id, the manufacturer trust-root signer profile (stella-manufacturer-release), and an expected canonical-payload hash.


Consumers

CLI — stella verify release-manifest

The CLI exposes the offline verifier as stella verify release-manifest (VerifyCommandGroup.BuildVerifyReleaseManifestCommand in src/Cli/StellaOps.Cli/Commands/VerifyCommandGroup.cs):

Gateway — product security/lifecycle metadata publication

ProductMetadataDocumentProvider (src/Router/StellaOps.Gateway.WebService/ProductMetadata/) reads a signed product update manifest from disk and uses it to publish .well-known/security.txt, the product-security JSON (stella.ops/productSecurityMetadata@v1), and the support-lifecycle JSON (stella.ops/productLifecycle@v1). Publication is fail-closed and is suppressed unless all of the following hold: PublishMetadata, SealedConfig, the operational Preflight.AllPassed, ReleaseManifestSignatureVerified, a valid GeneratedAtUtc, an HTTPS CanonicalBaseUrl, and a readable manifest file. When loading the manifest it additionally requires schemaVersion == stella.ops/productUpdateManifest@v1, supportPolicy.lifecycleSchema == stella.ops/productLifecycle@v1, that version equals the running product version, that images[] covers the running image digest (when configured), a non-empty madeAvailableInEu, and rejects any TBD- placeholder string. It only consumes a subset of fields (version, placedOnMarketAt, madeAvailableInEu, craApplicability, and the releaseTrain); the manifest’s signature is verified by the upstream pipeline, not by this provider.

Revision Log

Date (UTC)ChangeNotes
2026-05-30Doc/code reconciliation against StellaOps.Signer.Core.Pinned ManifestPayloadType/InTotoPayloadType constants and _type = https://in-toto.io/Statement/v0.1; expanded normalization, validation codes, and offline-verifier behavior to match ProductUpdateManifest.cs; clarified field requiredness; corrected the craApplicability cross-validation claim (not implemented); added CLI (stella verify release-manifest) and Gateway product-metadata consumer sections; noted the TBD- placeholder rule is enforced by the Gateway publisher, not the manifest builder.
2026-04-30Updated example release train to 2026.9.madeAvailableInEu = 2026-09-01, craApplicability = pre-cra, and lifecycle support dates run through 2031-09-01.
2026-04-30v1 introduced.Defines signed Stella release manifest schema and offline verifier expectations for CRA Annex I update-channel integrity.