Scanner plug-in signing
This page describes how authors produce a signed Scanner plug-in manifest and how operators curate the trust store that the Scanner runtime checks against. It mirrors the schema and behaviour shipped in StellaOps.Scanner.Plugin.Contracts and StellaOps.Scanner.Plugin.Loader.
Why signing
The Scanner host loads language-analyzer plug-ins out of a drop folder (typically plugins/scanner/analyzers/<lang>/). A plug-in is a manifest + an assembly DLL. By default the loader (ScannerPluginLoader) refuses to load any manifest without a valid DSSE signature whose key id resolves to a trust anchor in ScannerPluginLoaderOptions.TrustedKeys. This is the fail-secure posture from CoC §3 DoD.
The signing tool (StellaOps.Scanner.Plugin.SigningTool) is the build-time authoring half of that pipeline: it produces DSSE-signed manifest.json files. Runtime admission additionally checks the configured trust, payload and host contracts, and loadable dependencies. Runtime verification support (TrustStore and Ed25519AsymmetricSignatureVerifier) lives in StellaOps.Scanner.Plugin.Loader, so Worker/WebService hosts do not depend on the console/tooling assembly.
Runtime analyzer bundle contract
Scanner language analyzer bundles use a restart-time directory contract shared by Worker and WebService path resolution:
plugins/scanner/analyzers/
|-- dotnet/
| |-- manifest.json
| |-- StellaOps.Scanner.Analyzers.Lang.DotNet.dll
| |-- *.deps.json / runtimeconfig / transitive DLLs as required
|-- node/
| |-- manifest.json
| `-- ...
`-- <analyzer>/
|-- manifest.json
`-- payload files
Rules:
- The bundle root is
plugins/scanner/analyzersrelative to the application base directory unlessScannerAnalyzerHost__PluginDirectoryoverrides it. - Each analyzer is an immediate child directory of the root. The runtime does not recursively search nested bundle sets.
- Each child directory contains exactly one signed
manifest.json. The manifestassembly.pathis relative to that child directory and must not be absolute or contain... - Published bundles must include the analyzer DLL, transitive DLLs, deps and runtime files needed for
Assembly.LoadFrom, plus any analyzer-owned static data. The manifestassembly.sha256must bind the actual DLL bytes shipped in that assembled bundle. - Published manifests must also bind the host-owned contract assemblies that the analyzer was compiled against. The signing tool writes these signed metadata entries:
org.stellaops.contract-assembly.<assemblyName>=version=<assemblyVersion>;sha256=<digest>. Language analyzers require exact stamps forStellaOps.Scanner.Analyzers.Lang,StellaOps.Scanner.Analyzers.LangAdapter, andStellaOps.Scanner.Plugin.Contracts; OS analyzers requireStellaOps.Scanner.Analyzers.OS. - Worker compares those signed version/digest stamps with its own loaded contract assemblies after signature and payload-SHA verification, but before loading any plug-in assembly. A missing, malformed, unavailable, or mismatched contract rejects the bundle with a stable
contract_assembly.*reason code. Either analyzer host aborts activation if any bundle has such a rejection, so cross-load-context API skew cannot silently degrade a scan to zero components. - Production trust roots live under
etc/scanner/trust-store/scanner-pluginsrelative to the application base directory. Operators mount public keys there read-only. Private seeds never belong in production images or mounted trust stores. - The public keys under
src/__Tests/__Datasets/trust-store/scanner-plugins/devverify historical committed fixtures only. Mounted cohorts already exist in source and Compose; their normal signing and verification use the operator-approved identity and public trust described below. Fixture trust is not a fallback for that identity.
Base bundle contents declared by the source contract:
- OS analyzers:
apk,dpkg,rpm, loaded throughScannerOsAnalyzerHostfrom the signed base bundle root. - Language analyzers:
dotnet,golang,java,node,python.
Full-analyzer overlay contents declared by the source contract:
- the complete language set: base
dotnet,golang,java,node,pythonplus optionalbun,ccpp,dart,deno,elixir,php,ruby,rust, andswift.
docker-compose.plugins.scanner-full-analyzers.yml declares the full language set read-only at /app/plugins/scanner/analyzers and points ScannerAnalyzerHost__PluginDirectory at that canonical runtime root. The same overlay mounts the signed base bundle at /app/plugins/scanner/base for ScannerOsAnalyzerHost, so required OS analyzers stay signed and admitted without relying on raw build-output directories.
package-runtime-plugins.ps1 -Module scanner -Profile scanner-runtime publishes every language analyzer from the current source tree, rebuilds the base OS analyzers with their complete dotnet publish dependency closure, stamps the resulting host contracts, signs the manifests, and emits both the base and full-analyzer profiles. The RPM producer additionally fails packaging unless its managed SQLite provider and Linux native assets are present. The mounted build-service-publish.sh paths align both Scanner hosts after publishing the host: scanner-web builds the base profile, while scanner-worker builds the complete runtime profiles. Each host output receives the producer’s exact contract DLLs before image construction. Alignment is performed for every selected host; a prior web build must not cause the worker’s contract copy to be skipped. Do not assemble Scanner runtime bundles by copying an old bin/ or ignored analyzer drop tree. When only one host changes, stage its newly verified bundle in an immutable, owner-labelled runtime directory and bind that directory to the normal container bundle path. Replacing the shared live drop folder can invalidate another host’s still-running contract cohort.
The web cohort contains only the base language and OS bundle names declared by ScannerAnalyzerBundleContract. The generic producer also emits secrets for the Worker’s existing reflection loader; that bundle does not implement the signed Scanner manifest contract and must not be placed under the web verifier’s analyzer root. Do not suppress missing-manifest errors to admit it.
Harness overlay:
hello(stellaops.scanner.plugin.hello), built fromsrc/Scanner/samples/plugins/hello-langand signed with the samemanifest.json/assembly.sha256contract. It is the deterministic dummy analyzer for compose probe tests, not a production analyzer.
The Worker options types ScannerAnalyzerHostOptions and ScannerOsAnalyzerHostOptions default AllowUnsigned and the four Require* booleans to false. Thus signature checking is enabled by default, while an unconfigured Worker host can return an empty analyzer set for missing inputs. The committed docker-compose.scanner.yml, full-analyzer overlay and OS overlay already set the applicable RequirePluginDirectory, RequireTrustStoreDirectory, RequireTrustedKeys and RequireLoadedAnalyzers flags to true and name the mounted public trust directory. These configured Worker hosts refuse missing roots/trust or zero activated analyzers; contract-assembly rejections abort activation.
The WebService uses its separate ScannerSignedPluginVerifier: it logs missing roots/trust or rejected manifests and does not itself throw a startup failure for those conditions. The Worker option flags do not define that verifier’s policy. These are source and committed-Compose facts, not a claim that any particular live cohort, trust mount or image has been qualified.
Configured signing for normal builds
Normal Scanner packaging resolves the central scanner-plugin-manifest signature purpose through the canonical compliance profile and existing provider registry, preserving configured provider ordering and explicit pins. Existing installation files support Ed25519 or ES256, with independently admitted matching public trust through the raw Ed25519 or public SPKI index contract. The tool registers bouncycastle.ed25519, default, and the separately enabled fips.ecdsa.soft / eu.eidas.soft providers. Unsupported profile, provider, custody or Scanner wire selections fail closed. The separate RSA runtime-plugin-artifact contract does not sign Scanner DSSE manifests.
The operator supplies all five settings to package-runtime-plugins.ps1 or to the environment inherited by build-service-publish.sh:
| Environment setting | PowerShell parameter | Required input |
|---|---|---|
STELLAOPS_SCANNER_PLUGIN_SIGNING_KEY_FILE | ScannerSigningKeyPath | Existing protected installation key file produced through the central configured-material codec: base64 Ed25519 material or ES256 EC PEM |
STELLAOPS_SCANNER_PLUGIN_SIGNING_KEY_ID | ScannerSigningKeyId | Operator-approved key identity |
STELLAOPS_SCANNER_PLUGIN_SIGNING_PROVIDER | ScannerSigningProvider | Exact selected provider identity; bundled file-capable providers are bouncycastle.ed25519, default, and the separately enabled fips.ecdsa.soft / eu.eidas.soft |
STELLAOPS_SCANNER_PLUGIN_SIGNING_PROFILE | ScannerSigningProfile | Configured compliance profile, subject to canonical STELLAOPS_CRYPTO_COMPLIANCE_PROFILE precedence; unknown profiles fail closed |
STELLAOPS_SCANNER_PLUGIN_SIGNING_TRUST_STORE | ScannerSigningTrustStore | Independently admitted public-only directory: historical raw Ed25519 files or index.json entries with exact opaque key IDs and public SPKI |
The key file is authoritative: a missing, unreadable, malformed or conflicting file cannot fall back to a registered key or the tracked test seed. Resolution does not generate, enroll, replace or persist a private key. The normal packager does not export or admit public trust. It signs each staged unsigned manifest, stamps its actual payload and host contracts, and verifies the resulting DSSE signature against the configured public key before writing the manifest. Missing unsigned input is an error; copying an old signed manifest is not a fallback. -SignScannerAnalyzers source restamping is refused during packaging.
The explicit harness profile and protected -LabOnly procedure remain separate test/lab contracts. Their keys are not normal-build defaults or production trust. Signing-tool tests use isolated ephemeral private material; committed analyzer fixtures are checked through their public keys without reading their tracked seed.
Operator input request before candidate qualification
CCP-079 supplies source and isolated tests only. Before a real signing/build window, the operator must approve the Scanner purpose, provider/profile and key identity, provide an existing protected key-file pointer, and independently approve its matching raw public key and trust-directory pointer. Private material must stay outside source, image contexts and public trust directories. Supply pointers through the settings above; never paste key material into a receipt or command-line argument. No sanctioned Scanner identity is implied by a passing test. Enrollment of a new identity and admission of its public trust are separate operator actions, and normal build never performs them.
For an existing approved base64 Ed25519 key file, the supported preparation sequence is: use export-public below into a new, empty review directory; record the returned key ID and public-key SHA-256; independently approve that public identity and its intended Scanner purpose; then admit only the public files to the approved trust directory and configure the five settings. Use the returned key ID for ScannerSigningKeyId. Export does not establish that the identity is approved or admit it to runtime trust. The exporter writes its output files, so an existing trust directory must not be used as the review destination.
If no sanctioned pointer exists, provision-purpose is the explicit source-supported installation-file operation. It requires separate operator authority; normal lookup, signing and packaging never invoke it. The generic CLI key commands and enroll-runtime-key are not substitutes: the latter owns the different RSA runtime-bundle artifact contract. No live identity or trust follows from the provisioning tests.
Regional provisioning and public recovery
The tool composes the existing crypto DI options. Crypto:Compliance:ProfileId in --configuration <operator-json> is overridden by normal environment configuration, then by STELLAOPS_CRYPTO_COMPLIANCE_PROFILE. The effective value is validated before any lenient profile fallback. Algorithm selection uses scanner-plugin-manifest through SignatureProviderSelection; there is no algorithm CLI switch.
| Effective profile | Purpose algorithm | Installation-file capability |
|---|---|---|
world, international, kcmvp | Ed25519 | Existing BouncyCastle provider and base64 configured-material codec |
fips, eidas | ES256 | Existing EC providers and EC PEM configured-material codec |
gost, sm | Regional GOST / SM selection | Refused before generation: Scanner has no admitted wire token for that selection |
Unknown, blank or experimental pq | None admitted | Refused before generation; no world/default substitution |
Provider choice remains the registry’s separate configured ordering: StellaOps:Crypto:Registry:ActiveProfile, Profiles:<name>:PreferredProviders and PreferredProviders. --provider pins an exact provider. The first selected provider must itself support signing, verification and the existing material-generation capability; the tool does not search for another provider merely because that provider can generate. The bundled default registry can select default for ES256. Selecting a FIPS or eIDAS algorithm does not certify that provider. fips.ecdsa.soft requires FIPS_SOFT_ALLOWED=true; eu.eidas.soft requires EIDAS_SOFT_ALLOWED=true; both remain non-certified software. Production-blocked simulator/software identities are refused. Provider-held/non-exportable creation without an existing admitted installation custody mechanism is refused before generation; this tool neither exports their private material nor introduces a secret store.
Prepare an existing protected parent directory owned by the operator. Windows permits only owner/SYSTEM/Administrators access, refuses reparse ancestors and holds ancestor handles against rename/replacement. Linux requires an owner-only directory, descriptor-bound creation with no symlink following, and current-operator ownership; it requires /proc and statx. Other platforms fail closed. The file is reserved atomically with create-only semantics before key generation. Existing destinations, unsafe permissions and ambiguous paths are refused. The key ID is opaque; generated EC IDs contain sha256: and must never be converted to Windows filenames or renamed to make a filename fit.
After the separate operator grant, the explicit sequence is:
StellaOps.Scanner.Plugin.SigningTool provision-purpose --purpose scanner-plugin-manifest --configuration <operator-config> --provider <selected-provider> --key-file <existing-protected-parent/scanner.key>
StellaOps.Scanner.Plugin.SigningTool export-purpose-public --purpose scanner-plugin-manifest --configuration <same-config> --provider <same-provider> --key-id <exact-returned-id> --key-file <same-private-file> --out <public-review-directory/index.json>
The first command creates only the private file and reports non-secret profile/provider/key identity. The second reopens that same identity through the configured resolver and writes public review material create-only. It does not admit trust. Independently review the public SPKI fingerprint, exact key ID, purpose and provider, then admit only the approved public index into the runtime trust directory. Index entries bind keyId, algorithm and publicKeyBase64; SPKI supports the existing EdDSA/ES256/RS256/PS256 verifier wire. RSA verification support does not create a new RSA provisioning choice. Conflicting raw/index keys and duplicate index identities are refused.
A public-export failure leaves the private file untouched. Retry only export-purpose-public with the same key and a new output destination; it recovers the same public identity. Generation or partial-write failure leaves the reserved file in place and returns failure. Do not overwrite it or silently regenerate: an operator must first reconcile the incomplete reservation. Normal sign-purpose always honors the configured file, rejects conflicts with provider state, signs through the selected provider and verifies against independent public trust before producing a manifest.
After those inputs and build-signing authority exist, qualify both Scanner hosts and their analyzer cohorts together from source containing CCP-075 and CCP-079. Record actual image/cohort IDs and forward recovery before asking for the exact CCP-075 custody transfer. This source integration does not qualify images or authorize deployment.
Verbs
dotnet exec .../StellaOps.Scanner.Plugin.SigningTool.dll <verb> ...
| Verb | Purpose |
|---|---|
sign-purpose | Resolve the configured Scanner purpose key, sign, and verify against configured public trust before emitting the manifest. |
provision-purpose | Explicit create-only protected installation enrollment under configured regional policy; separate operator authority required. |
export-purpose-public | Reopen the established purpose identity and export its public index create-only; retry without generating or admitting trust. |
sign | Explicit isolated author/test/lab signing with supplied Ed25519 material; not used by normal packaging. |
verify | Read a signed manifest, look up each signature’s key id in a trust-store directory, exit 0 iff at least one signature verifies. |
derive-keyid | Print the conventional key id for an Ed25519 public key file. |
export-public | Derive public material from a private seed, or take the raw 32-byte public key with --public-key-file, and write a loader-compatible trust-store entry without exposing the seed. |
sign-external | Sign with an Ed25519 key held outside the process: an external command (--external-signer, --external-signer-arg) signs the DSSE PAE bytes, and the result must verify against --trust-store before the manifest is written. The release pipeline uses it to keep the plugin release key on the deployment host. |
sign-purpose
dotnet exec StellaOps.Scanner.Plugin.SigningTool.dll sign-purpose \
--purpose scanner-plugin-manifest --profile world \
--provider bouncycastle.ed25519 --key-id <approved-key-id> \
--key-file <protected-base64-key-file> --trust-store <approved-public-trust-directory> \
--manifest <unsigned-manifest> --stamp-assembly-sha256 \
--assembly-base-dir <staged-bundle> --contract-assembly <required-contract-dlls> \
--out <staged-bundle>/manifest.json
Every custody option is required. Wrong purpose, unsupported profile/provider, missing key or public trust, and a signature/public-key mismatch return input error 3 without writing a signed manifest. This command never changes public trust.
sign (isolated author/test/lab use)
dotnet exec StellaOps.Scanner.Plugin.SigningTool.dll sign \
--manifest plugins/scanner/analyzers/elixir/manifest.unsigned.json \
--key <isolated-ephemeral-key> \
--stamp-assembly-sha256 \
--assembly-base-dir plugins/scanner/analyzers/elixir \
--contract-assembly path/to/StellaOps.Scanner.Analyzers.Lang.dll \
path/to/StellaOps.Scanner.Analyzers.LangAdapter.dll \
path/to/StellaOps.Scanner.Plugin.Contracts.dll \
--out plugins/scanner/analyzers/elixir/manifest.json
--manifest— path to the unsigned manifest JSON. Must match theScannerPluginManifestschema exposed byPlugin.Contracts(the signing tool validates the same invariants the loader does — relativeassembly.path, no..traversal, required fields, etc. — and refuses to sign garbage with a clear diagnostic).--key— path to the Ed25519 private key file. Accepts 32-byte raw seeds, 64-byte libsodium expanded secrets, hex of either, or base-64 of either.--key-id— optional. When omitted the tool derives the conventional key id from the public key.--key-id-prefix— optional. Override the defaultstella-scanner-plugin-dev-prefix for an isolated author/test/lab identity. Normalsign-purposerequires the explicit approved key ID instead.--stamp-assembly-sha256and--assembly-base-dir— computeassembly.sha256from the staged payload rather than trusting an old manifest value.--contract-assembly— one or more host-owned contract DLLs. The supplied assembly names must exactly match the required language or OS set; missing, duplicate, or extra contract assemblies fail signing.--out— destination path for the signed manifest.
verify
dotnet exec StellaOps.Scanner.Plugin.SigningTool.dll verify \
--manifest <staged-operator-signed-manifest> \
--trust-store <approved-public-trust-directory>
Exit codes:
0— manifest verified against a trusted key.4— verification failed (tampered payload, unknown key, bad algorithm token, etc.). The stable reason code is printed on stderr (signature.payload_mismatch,signature.verify.failed, …).3— input error (file not found, manifest invalid JSON, …).
derive-keyid (historical public-fixture diagnostic)
This example checks the known public fixture’s naming convention only. Its output is not the identity of a newly operator-signed bundle and grants no trust.
dotnet exec StellaOps.Scanner.Plugin.SigningTool.dll derive-keyid \
--key src/__Tests/__Datasets/trust-store/scanner-plugins/dev/stella-scanner-plugin-dev-ef2eaaad7b347fe5.public.ed25519
# prints: stella-scanner-plugin-dev-ef2eaaad7b347fe5
export-public
dotnet exec StellaOps.Scanner.Plugin.SigningTool.dll export-public \
--key <approved-protected-base64-key-file> \
--output-directory <reviewed-public-export-directory> \
--key-id-prefix stella-scanner-plugin-release-
The command writes the raw <keyId>.public.ed25519 file consumed by TrustStore, a base-64 mirror, and index.json. It never writes or prints the private seed. Run it only under the separate operator authority for that key and public export. Writing an export does not admit it into a running host’s trust store.
Key id convention
keyId = <prefix> + first16hex(SHA-256(rawPublicKey))
The derivation helpers default to stella-scanner-plugin-dev-; an operator deriving a new approved identity must supply the intended prefix explicitly. Normal sign-purpose uses its configured key ID without generating or renaming it.
This convention is deterministic: an operator can rebuild the keyId -> publicKey mapping from public keys alone, which means key rotation can be audited without trusting the file names on disk.
Trust-store layout
<approved-public-trust-directory>/
├── index.json
├── <approved-key-id>.public.b64
└── <approved-key-id>.public.ed25519
<keyid>.public.ed25519— raw 32-byte Ed25519 public key (binary). The runtime trust-store reader inStellaOps.Scanner.Plugin.Loader(TrustStore.LoadFromDirectory) keys off this file’s name prefix.<keyid>.public.b64— mirror in base-64. Optional but convenient for copy-paste / log redaction.index.json— canonical JSON metadata (key id, algorithm, hex/base64 public key, SHA-256 of the public key for cross-checks).- Only public material belongs here. Private keys and the tracked test seed must not be copied into a runtime trust directory or image.
A Scanner host wires the Loader trust store into the loader like this:
var trusted = TrustStore.LoadFromDirectory(trustStoreDirectory);
var options = new ScannerPluginLoaderOptions
{
PluginDirectory = pluginDirectory,
SearchPattern = "manifest.json", // or the default "*.manifest.json"
TrustedKeys = trusted,
AllowUnsigned = false, // production posture; CoC §3 DoD
};
var loader = new ScannerPluginLoader(options, new Ed25519AsymmetricSignatureVerifier());
var result = await loader.LoadAsync(cancellationToken);
ScannerAnalyzerBundleContract.ProductionTrustStoreRelativePath supplies the default relative location. The committed overlays explicitly select /app/trust-roots/plugins/scanner; operators must bind the approved public-only directory to the actual configured location for their selected cohort.
Production posture
- Use an operator-provisioned identity through the configured-purpose path; never use the committed test seed for normal packages. The current CLI bundles
bouncycastle.ed25519; additional provider admission requires its own supported integration and qualification, not an arbitrary provider-name change. - Run the signing tool inside the same supply-chain perimeter that produces the plug-in DLL; ideally as the same MSBuild target so the signed manifest is part of the publish output.
- Rebuild and re-sign the bundle whenever a host contract changes. For the in-repo Worker use
devops/docker/build-service-publish.sh scanner-worker; acontract_assembly.mismatchrejection is rebuild guidance, not a switch to bypass. - Publish only the public-key half of each rotated key to operators; the private half stays in the signer.
- Never enable
AllowUnsigned=trueoutside dev/CI. The loader emits a WARNING log when that path is taken (loader.manifest.allow_unsigned) so operators can audit for accidental drift.
Determinism
Ed25519 signatures are deterministic by RFC 8032 construction. Re-signing the same manifest with the same key produces byte-identical signature bytes. Normal packaged cohorts are build output under .runtime/plugins/; source fixtures and their historical public signatures remain separate.
The canonical-payload computation uses StellaOps.Canonical.Json’s RFC 8785 canonicalization (the same library the verifier consumes), so the canonical bytes the signing tool covers are byte-identical to the canonical bytes the loader reconstructs from the in-record manifest core.
Code-of-conduct notes
- Normal signing goes through central
ICryptoSignerand the configured provider; DSSE verification and the explicit lab signer useStellaOps.Cryptography.Profiles.EdDsa. No private-key parsing or cryptographic primitive is implemented in packaging. - Runtime hosts use
StellaOps.Scanner.Plugin.Loaderfor trust-store reading and Ed25519 verification. The signing tool is build-time authoring support and must not be a Worker/WebService runtime dependency. - The normal configured-file path supports Ed25519 and ES256 purpose selection. The Loader, Worker language/OS hosts and Web verifier consume public index/SPKI through the existing verifier. Other regional selections are explicitly refused; loading a provider does not establish a new Scanner wire format.
- The signing tool injects
TimeProvider; noDateTime.UtcNowis used.
Author quick-start
Third-party analyzer authors should start from the docs/dev/templates/scanner-analyzer/ template (Hello-Lang). The template ships with a working signed manifest and a worked-example integration test under StellaOps.Scanner.Plugin.Loader.Tests/HelloLangTemplateIntegrationTests.cs.
Operator workflow (stella scanner plugins)
The signing tool resolves existing private custody for an authorized build; it does not enroll or own the operator’s key. Operators checking an already-signed cohort use stella scanner plugins. These commands reuse the Loader and trust reader, but a local result does not establish the selected runtime host’s activation, contract compatibility or deployment state.
For the normal examples, set APPROVED_PLUGIN_DIR to the selected staged cohort, APPROVED_SCANNER_TRUST_DIR to its independently approved public-only trust directory, and APPROVED_PLUGIN_MANIFEST to the specific manifest being checked. Use the public identity selected for that cohort, not the repository’s dev trust.
stella scanner plugins list <plugin-dir> [--trust-store <dir>] [--pattern <glob>] [--allow-unsigned] [--format text|json]
stella scanner plugins verify --manifest <path> [--trust-store <dir>] [--strict] [--format text|json]
stella scanner plugins derive-keyid --public-key <path> [--prefix <prefix>] [--format text|json]
list
Enumerate plug-ins under a directory. One signed manifest.json is expected per immediate subdirectory (the drop-folder convention used by plugins/scanner/analyzers/<lang>/).
stella scanner plugins list "$APPROVED_PLUGIN_DIR" \
--trust-store "$APPROVED_SCANNER_TRUST_DIR"
Text output reports the observed plug-in IDs, versions, language capabilities, signature status, key IDs and payload digests. Compare them with the approved cohort inventory; the historical fixture’s key ID is not an expected normal result.
Exit codes:
0— at least one plug-in loaded and zero rejections.1— one or more plug-ins were rejected (fail-secure; CoC §3 DoD), or the directory contained no manifests at all.2— input error (directory missing, trust store malformed, etc.).
--format json produces a deterministic JSON document with success, count, rejectedCount, plugins[], and rejected[] arrays; useful for CI gates.
verify
Verify a single signed manifest against a trust-store directory. Mirrors the SigningTool’s verify verb but lives in the operator CLI so it doesn’t drag the signing-tool binary onto production hosts.
stella scanner plugins verify \
--manifest "$APPROVED_PLUGIN_MANIFEST" \
--trust-store "$APPROVED_SCANNER_TRUST_DIR"
The default posture is --strict (rejects unsigned manifests). Keep it enabled for normal operation and key rotation. --strict=false permits unsigned input and belongs only to explicitly isolated tests.
Exit codes match list.
derive-keyid
Operator-side convenience over the public key-ID derivation helper. Supply an approved public-key file and the explicitly chosen identity prefix; compare the result to the key ID configured for signing before using that trust entry.
stella scanner plugins derive-keyid \
--public-key "$APPROVED_SCANNER_PUBLIC_KEY" --prefix "$APPROVED_SCANNER_KEY_PREFIX"
APPROVED_SCANNER_PUBLIC_KEY and APPROVED_SCANNER_KEY_PREFIX are explicit operator inputs. Omitting --prefix selects the helper’s dev naming convention; it does not select or approve a production identity.
Smoke recipe
After staging a purpose-signed cohort, check it with the same approved public trust selected for its signatures:
stella scanner plugins list "$APPROVED_PLUGIN_DIR" \
--trust-store "$APPROVED_SCANNER_TRUST_DIR" --format json
Require exit code 0, success: true, rejectedCount: 0, the exact expected nonzero cohort count, and the approved key IDs on the signed entries. Success is conditional on the actual signatures, payloads and loadable dependencies passing; it is not guaranteed merely because signing ran. This local check does not replace qualification of the deployed web/worker cohort or authorize trust changes.
Source verification
Checked against 17cdc1a44a483a2594515350f32de0f5b4a72d4d on 2026-10-13. Recheck defaults and committed deployment flags with:
rg -n 'AllowUnsigned|RequirePluginDirectory|RequireTrustStoreDirectory|RequireTrustedKeys|RequireLoadedAnalyzers' \
src/Scanner/StellaOps.Scanner.Worker/Plugins/ScannerAnalyzerHostOptions.cs \
src/Scanner/StellaOps.Scanner.Worker/Plugins/ScannerOsAnalyzerHostOptions.cs \
devops/compose/docker-compose.scanner.yml \
devops/compose/docker-compose.plugins.scanner-full-analyzers.yml \
devops/compose/docker-compose.plugins.scanner-os.yml
For WebService refusal/logging behavior, inspect src/Scanner/StellaOps.Scanner.WebService/Hosting/ScannerSignedPluginVerifier.cs. For CLI defaults and result fields, inspect CommandFactory.cs (the HandleScannerPluginsVerifyAsync registration) and CommandHandlers.ScannerPlugins.cs under src/Cli/StellaOps.Cli/Commands/. Source and Compose checks do not establish which configuration a running deployment currently uses.
