Offline Kit Parity for Scanner Standards (SC10)

Status: Draft · Date: 2025-12-04 Scope: Define offline-kit contents, DSSE signing, and parity requirements for schemas, adapters, mappings, and fixtures to enable air-gapped operation.

Objectives

Bundle Structure

out/offline/scanner-standards-kit-v1/
├── manifest.json                    # Bundle manifest with all hashes
├── manifest.dsse                    # DSSE signature over manifest
├── schemas/
│   ├── cyclonedx-1.7.schema.json   # CDX 1.7 JSON schema
│   ├── cyclonedx-1.6.schema.json   # CDX 1.6 JSON schema
│   ├── spdx-3.0.1.schema.json      # SPDX 3.0.1 JSON-LD schema
│   └── slsa-provenance-v1.schema.json
├── adapters/
│   ├── mapping-cvss4-to-cvss3.csv
│   ├── mapping-cdx17-to-cdx16.csv
│   ├── mapping-slsa12-to-slsa10.csv
│   └── hashes.txt
├── fixtures/
│   ├── cdx17-cbom/
│   │   ├── sample-cdx17-cbom.json
│   │   ├── sample-cdx16.json
│   │   ├── source-track.sample.json
│   │   └── hashes.txt
│   └── competitor-adapters/
│       ├── fixtures/
│       │   ├── normalized-syft.json
│       │   ├── normalized-trivy.json
│       │   └── normalized-clair.json
│       └── coverage.csv
├── tools/
│   ├── versions.json                # Pinned tool versions
│   └── checksums.txt                # Tool binary hashes
└── trust/
    ├── root-ca.pem                  # Trust root for signature verification
    └── keyring.json                 # Signing key metadata

Manifest Format

{
  "version": "1.0.0",
  "created": "2025-12-04T00:00:00Z",
  "creator": "stellaops-scanner",
  "artifacts": [
    {
      "path": "schemas/cyclonedx-1.7.schema.json",
      "type": "schema",
      "format": "cyclonedx",
      "version": "1.7",
      "blake3": "a1b2c3d4...",
      "sha256": "e5f6a7b8..."
    },
    {
      "path": "adapters/mapping-cvss4-to-cvss3.csv",
      "type": "adapter",
      "source": "cvss4",
      "target": "cvss3.1",
      "blake3": "fa600b26...",
      "sha256": "072b66be..."
    },
    {
      "path": "fixtures/cdx17-cbom/sample-cdx17-cbom.json",
      "type": "fixture",
      "format": "cyclonedx",
      "version": "1.7",
      "blake3": "27c6de0c...",
      "sha256": "22d8f6f8..."
    }
  ],
  "tools": {
    "syft": {
      "version": "1.0.0",
      "blake3": "...",
      "sha256": "..."
    },
    "trivy": {
      "version": "0.50.0",
      "blake3": "...",
      "sha256": "..."
    }
  },
  "manifestHash": {
    "blake3": "...",
    "sha256": "..."
  }
}

DSSE Signing

Offline kit and signed-SBOM verification tests must use real local DSSE signatures. Envelope shape, JWS headers, placeholder certificates, Rekor UUIDs, and Fulcio issuer strings are not proof; Scanner records them as unavailable or invalid until the relevant local trust-root/Rekor verifier succeeds.

For local Rekor transparency, the offline kit must carry a Rekor receipt, a signed checkpoint, and a local Rekor public key. Scanner verifies that the receipt is bound to the DSSE envelope bytes, the checkpoint signature verifies against the local key, and the Merkle path recomputes the checkpoint root. It does not call online Rekor during offline-kit import.

Signature Format

{
  "payloadType": "application/vnd.stellaops.scanner.manifest+json",
  "payload": "<base64-encoded-manifest>",
  "signatures": [
    {
      "keyid": "stellaops-scanner-release-2025",
      "sig": "<base64-signature>"
    }
  ]
}

Signing Process

#!/bin/bash
# scripts/scanner/sign-offline-kit.sh

MANIFEST="out/offline/scanner-standards-kit-v1/manifest.json"
DSSE="out/offline/scanner-standards-kit-v1/manifest.dsse"
KEY_ID="stellaops-scanner-release-2025"

# Compute manifest hash
MANIFEST_HASH=$(b3sum "${MANIFEST}" | cut -d' ' -f1)

# Sign with DSSE
stellaops-sign dsse \
    --payload-type "application/vnd.stellaops.scanner.manifest+json" \
    --payload "${MANIFEST}" \
    --key-id "${KEY_ID}" \
    --output "${DSSE}"

echo "Signed manifest: ${DSSE}"
echo "Manifest BLAKE3: ${MANIFEST_HASH}"

Verification

Offline Verification Steps

#!/bin/bash
# scripts/scanner/verify-offline-kit.sh

KIT_DIR="out/offline/scanner-standards-kit-v1"

# 1. Verify DSSE signature
stellaops-verify dsse \
    --envelope "${KIT_DIR}/manifest.dsse" \
    --trust-root "${KIT_DIR}/trust/root-ca.pem" \
    --expected-payload-type "application/vnd.stellaops.scanner.manifest+json"

# 2. Extract manifest and verify artifacts
MANIFEST=$(stellaops-verify dsse --envelope "${KIT_DIR}/manifest.dsse" --extract-payload)

# 3. Verify each artifact hash
for artifact in $(echo "${MANIFEST}" | jq -r '.artifacts[] | @base64'); do
    path=$(echo "${artifact}" | base64 -d | jq -r '.path')
    expected_blake3=$(echo "${artifact}" | base64 -d | jq -r '.blake3')

    actual_blake3=$(b3sum "${KIT_DIR}/${path}" | cut -d' ' -f1)

    if [[ "${actual_blake3}" != "${expected_blake3}" ]]; then
        echo "FAIL: ${path} hash mismatch"
        exit 1
    fi
    echo "PASS: ${path}"
done

echo "All artifacts verified"

Programmatic Verification

// src/Scanner/StellaOps.Scanner.Offline/OfflineKitVerifier.cs
public class OfflineKitVerifier
{
    public async Task<VerificationResult> VerifyAsync(
        string kitPath,
        ITrustRootProvider trustRoots)
    {
        var manifestPath = Path.Combine(kitPath, "manifest.json");
        var dssePath = Path.Combine(kitPath, "manifest.dsse");

        // Verify DSSE signature
        var envelope = await DsseEnvelope.LoadAsync(dssePath);
        var signatureValid = await _verifier.VerifyAsync(envelope, trustRoots);

        if (!signatureValid)
            return VerificationResult.SignatureInvalid;

        // Parse manifest
        var manifest = JsonSerializer.Deserialize<OfflineManifest>(
            envelope.Payload);

        // Verify each artifact
        foreach (var artifact in manifest.Artifacts)
        {
            var artifactPath = Path.Combine(kitPath, artifact.Path);
            var actualHash = await HashUtil.Blake3Async(artifactPath);

            if (actualHash != artifact.Blake3)
                return VerificationResult.ArtifactHashMismatch(artifact.Path);
        }

        return VerificationResult.Success(manifest);
    }
}

Parity Requirements

Required Contents

CategoryOnlineOffline KitNotes
CDX 1.7 SchemaCDN fetchBundledSchema validation
CDX 1.6 SchemaCDN fetchBundledDowngrade validation
CVSS v4→v3 AdapterAPI lookupBundled CSVPure function
CDX 1.7→1.6 AdapterAPI lookupBundled CSVPure function
SLSA 1.2→1.0 AdapterAPI lookupBundled CSVPure function
Golden FixturesTest repoBundledDeterminism tests
Tool BinariesPackage registryBundled/ChecksumSyft, Trivy
Trust RootsOnline PKIBundled PEMSignature verification

Functional Parity

The offline kit must support:

OperationOnlineOffline
Schema validationYesYes
SBOM serializationYesYes
Downgrade conversionYesYes
Hash verificationYesYes
DSSE verificationYesYes
Determinism testingYesYes
Rekor transparencyYesYes*

*Offline mode uses mirrored checkpoints instead of live Rekor

Bundle Generation

CI Workflow

# .gitea/workflows/offline-kit.yml
jobs:
  build-offline-kit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Collect schemas
        run: |
          mkdir -p out/offline/scanner-standards-kit-v1/schemas
          cp schemas/cyclonedx-*.json out/offline/scanner-standards-kit-v1/schemas/
          cp schemas/spdx-*.json out/offline/scanner-standards-kit-v1/schemas/

      - name: Collect adapters
        run: |
          mkdir -p out/offline/scanner-standards-kit-v1/adapters
          cp docs/modules/scanner/fixtures/adapters/*.csv out/offline/scanner-standards-kit-v1/adapters/
          cp docs/modules/scanner/fixtures/adapters/hashes.txt out/offline/scanner-standards-kit-v1/adapters/

      - name: Collect fixtures
        run: |
          mkdir -p out/offline/scanner-standards-kit-v1/fixtures
          cp -r docs/modules/scanner/fixtures/cdx17-cbom out/offline/scanner-standards-kit-v1/fixtures/
          cp -r docs/modules/scanner/fixtures/competitor-adapters out/offline/scanner-standards-kit-v1/fixtures/

      - name: Generate manifest
        run: scripts/scanner/generate-manifest.sh

      - name: Sign bundle
        run: scripts/scanner/sign-offline-kit.sh
        env:
          SIGNING_KEY: ${{ secrets.SCANNER_SIGNING_KEY }}

      - name: Verify bundle
        run: scripts/scanner/verify-offline-kit.sh

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: scanner-standards-kit-v1
          path: out/offline/scanner-standards-kit-v1/

Update Procedure

Offline Kit Refresh

When schemas/adapters change:

  1. Update source artifacts in repository
  2. Run hash verification CI
  3. Generate new manifest
  4. Sign with release key
  5. Publish new kit version
  6. Document changes in release notes

Version Compatibility

Kit VersionScanner VersionCDX VersionCVSS Support
v1.0.01.x1.6, 1.7v3.1, v4.0
v1.1.01.x1.6, 1.7, 1.8*v3.1, v4.0

*Future versions