Release notes — Excititor signature verification ingest gate
Sprint: 20260501-020 Audit: docs-archive/qa/audits/microservice-audit-pass2-2026-04-29.md §E1 ADR: docs/architecture/decisions/ADR-019-excititor-vex-signature-verification-ingest-gate.md Status: Pre-prod; breaking changes explicitly authorized by user (2026-04-29).
Summary
Audit pass 2 finding §E1 documented that the Excititor connector pipeline admitted unsigned and tampered VEX documents because the default IVexSignatureVerifier binding was a no-op (VerificationDisabledVexSignatureVerifier) and VexConnectorBase never gated raw documents on verification. Sprint 20260501-020 closes the gap by:
- Making
VexConnectorBase.CreateRawDocumentAsyncperform mandatory verification before the digest is computed. - Deleting
VerificationDisabledVexSignatureVerifierand routing every V1 call throughVexSignatureVerifierV1Adapter, which callsIVexSignatureVerifierV2and throwsVexSignatureRequiredExceptionon missing/invalid signatures. - Adding a hosted startup validator that refuses to start the host when
IHostEnvironment.IsProduction()is true andVexSignatureVerification:Connectors:AllowUnsigned=true.
All cryptography is delegated to StellaOps.Cryptography.ICryptoProviderRegistry; no new primitives were introduced.
Removed types
StellaOps.Excititor.Core.VerificationDisabledVexSignatureVerifier(the legacy V1 no-op default — the canonical anti-pattern called out by audit §E1).StellaOps.Excititor.WebService.Services.VexSignatureVerifierV1Adapter(moved intoStellaOps.Excititor.Core.Verification; the new copy is fail-closed where the WebService copy was best-effort).
New types
StellaOps.Excititor.Core.Verification.VexSignatureRequiredExceptionStellaOps.Excititor.Core.Verification.VexConnectorVerificationOptionsStellaOps.Excititor.Core.Verification.VexConnectorVerificationGlobalOptionsStellaOps.Excititor.Core.Verification.VexConnectorVerificationStartupValidatorStellaOps.Excititor.Core.Verification.VexSignatureMetadataKeysStellaOps.Excititor.Core.Verification.VexSignatureVerifierV1Adapter(moved)StellaOps.Excititor.Core.Storage.IVexQuarantineSinkStellaOps.Excititor.Core.Storage.VexQuarantineEntryStellaOps.Excititor.Persistence.Postgres.Repositories.PostgresVexQuarantineSink
Changed types
VexConnectorBasegainsprotected ValueTask<VexRawDocument> CreateRawDocumentAsync(...).VexConnectorBase.CreateRawDocument(...)(sync overload) is[Obsolete(DiagnosticId = "EXCITITOR-VRF-01")]. Promotion toerror: truelands in sprint 20260501-021 once CSAF connectors migrate.
Migration: configuration
# Before (silently accepted unsigned VEX):
VexSignatureVerification:
Enabled: false
# After: production deployment.
VexSignatureVerification:
Enabled: true
DefaultProfile: world # or fips/gost/sm depending on regional crypto
IssuerDirectory:
ServiceUrl: https://issuer-directory.<your-domain>
Timeout: 00:00:05
Valkey:
ConnectionString: "valkey:6379,abortConnect=false"
Database: 5
KeyPrefix: "excititor:vex-verification:"
Connectors:
AllowUnsigned: false # default; setting true in production WILL refuse to start
Migration: code
Before (legacy, silently accepted unsigned)
public override async IAsyncEnumerable<VexRawDocument> FetchAsync(
VexConnectorContext context,
[EnumeratorCancellation] CancellationToken ct)
{
await foreach (var doc in _fetcher.FetchAsync(...))
{
var raw = CreateRawDocument(VexDocumentFormat.Csaf, doc.Uri, doc.Bytes, metadata);
await context.RawSink.StoreAsync(raw, ct);
yield return raw;
}
}
After (fail-closed)
public override async IAsyncEnumerable<VexRawDocument> FetchAsync(
VexConnectorContext context,
[EnumeratorCancellation] CancellationToken ct)
{
var verification = new VexConnectorVerificationOptions
{
// Defaults are fail-closed; only set AllowUnsigned in non-production.
AllowUnsigned = false,
CryptoProfile = "world",
};
await foreach (var doc in _fetcher.FetchAsync(...))
{
var raw = await CreateRawDocumentAsync(
context,
VexDocumentFormat.Csaf,
doc.Uri,
doc.Bytes,
metadata,
verification,
ct);
await context.RawSink.StoreAsync(raw, ct);
yield return raw;
}
}
The OCI connector (OciOpenVexAttestationConnector) is updated in this sprint as the reference implementation. CSAF connectors (RedHat, Cisco, Oracle, Ubuntu, MSRC, Rancher) are migrated in sprint 20260501-021.
Migration: tests
Test fixtures that registered VerificationDisabledVexSignatureVerifier explicitly will fail to compile. Replace with a small fake that returns null for “unsigned” or a populated VexSignatureMetadata for “trusted”:
private sealed class FakeVexSignatureVerifier : IVexSignatureVerifier
{
public VexSignatureMetadata? Result { get; set; }
public ValueTask<VexSignatureMetadata?> VerifyAsync(VexRawDocument document, CancellationToken ct)
=> ValueTask.FromResult(Result);
}
Set Cosign:RequireSignature=false in connector settings (or set VexConnectorVerificationOptions.AllowUnsigned=true directly) for tests that want to exercise the legacy “fixture has no real signature” path without provoking a fail-closed exception.
Operator checklist
- Configure
VexSignatureVerification:IssuerDirectory:ServiceUrlto point at a real Issuer Directory deployment (no offline/in-memory fallback in live runtimes). - Confirm
VexSignatureVerification:Connectors:AllowUnsignedis unset orfalsein your production environment file. The startup validator throws otherwise. - Migrate any custom connector built on
CreateRawDocumenttoCreateRawDocumentAsyncbefore the next sprint promotes the obsolete diagnostic toerror: true. - Subscribe alerting rules to the new metric tag set:
excititor.connector.signature.verified{result=verified|unsigned|failed}andexcititor.connector.signature.failure_reason{reason=...}. - If you enable
QuarantineOnFailure, verifyvex.excititor_quarantineexists after startup migrations and reviewdocs/ops/excititor-quarantine.mdfor inspect, replay, and purge handling.
Sprint 20260501-022 addendum — CSAF schema validation as a hard gate
In addition to signature verification, CSAF connectors now run a fail-closed schema validator (CsafSchemaValidator) immediately after signature verification and before raw-sink storage. Documents that pass the signature gate but fail schema validation are rejected with CsafSchemaValidationException carrying structured diagnostics, and the quarantine reason is csaf-schema-invalid:<issue-code>.
Breaking implication
Documents that previously slipped through with malformed shape — e.g. MsrcCsafConnector accepted any well-formed JSON, RedHat/Oracle deferred to the normalizer — are now quarantined at fetch time. Operators may see quarantine rows where ingestion previously succeeded silently with half-populated normalization output.
Sample diagnostic
A malformed MSRC document missing document.tracking.id produces a CsafSchemaValidationException with diagnostics:
csaf.validation.issueCode = csaf-missing-mandatory-key
csaf.validation.pointer = /document/tracking/id
csaf.validation.message = Mandatory property '/document/tracking/id' is missing.
csaf.validation.csafVersion = 2.0
Quarantine reason published to the quarantine sink: csaf-schema-invalid:csaf-missing-mandatory-key.
Configuration
The validator binds defaults from CsafSchemaOptions. Override per tenant via excititor.formats.csaf:* keys:
excititor.formats.csaf:
supportedVersions: ["2.0", "2.1"] # CSAF version allowlist
maxProductTreeDepth: 8 # DoS bound on product_tree.branches recursion
collectAllIssues: false # default fail-fast
Issue codes (stable wire contract — do not rename): csaf-schema-version-unsupported, csaf-missing-mandatory-key, csaf-cve-malformed, csaf-product-tree-too-deep, csaf-malformed-json.
The OASIS CSAF 2.0 schemas are embedded as resources in StellaOps.Excititor.Formats.CSAF.dll (vendored from docs/contracts/schemas/eu/csaf/2.0/). Air-gap installs and CI need no network access; runtime never fetches schemas.
Operator checklist (sprint 022 additions)
- Confirm
excititor.formats.csaf:supportedVersionsmatches the issuers in your feed list. Restricting the allowlist to["2.0"](omitting2.1) is supported and explicitly tested. - Subscribe alerting rules to the new diagnostic family:
csaf.validation.issueCode{value=csaf-missing-mandatory-key|csaf-schema-version-unsupported|csaf-cve-malformed|csaf-product-tree-too-deep|csaf-malformed-json}. - Confirm
ICsafSchemaValidatoris registered in the connector host. The worker does this throughAddCsafNormalizer(), which also registers the schema validator.
Sprint 20260502-026 addendum - quarantine sink
The quarantine sink is now implemented:
- Startup migration:
010_excititor_quarantine.sql. - Table:
vex.excititor_quarantine. - Default retention: 30 days (
VexConnectorVerificationOptions.QuarantineRetention). - Tenant isolation: RLS on
tenant = vex_app.require_current_tenant(). - Base-class routing: signature failures and CSAF schema failures write a row when
QuarantineOnFailure=true, then rethrow.
The row schema stores payload bytea, digest, reason, and diagnostics jsonb; operators should use the runbook instead of directly copying payloads into the raw store.
