ADR-019: VEX signature verification as a mandatory ingest gate
Formerly
docs/architecture/ADR-EXCITITOR-VEX-SIGNATURE-VERIFICATION-INGEST-GATE.md.
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-05-01 |
| Sprint | SPRINT_20260501_020_Excititor_signature_verifier_interface (archived after acceptance) |
| Audit source | microservice-audit-pass2-2026-04-29.md §E1 |
| Owners | Excititor team, Project Manager |
| Supersedes | n/a |
| Related | SPRINT_20260501_021…023 (CSAF wiring, schema validation, AOC guard) |
For: engineers on the Stella Ops Concelier/Excititor VEX pipeline and reviewers who need to understand why every VEX connector must verify a document’s signature before it reaches the raw sink. This ADR makes signature verification a hard ingest gate so that a tampered or unsigned VEX document cannot be normalized and weighted as if it were trusted.
Context
Audit pass 2 (advisory §E1) found that none of the six CSAF VEX connectors (RedHatCsafConnector, UbuntuCsafConnector, OracleCsafConnector, CiscoCsafConnector, MsrcCsafConnector, RancherHubConnector) called any signature verifier before storing raw documents, and the only connector that did invoke a verifier (OciOpenVexAttestationConnector) treated the result as advisory: it captured signatureMetadata but never rejected the document when verification returned null or failed. The default IVexSignatureVerifier binding was VerificationDisabledVexSignatureVerifier, a class that unconditionally returned null and was registered as the production fallback in StellaOps.Excititor.WebService.Program. The net effect was that a tampered or unsigned VEX document reached the raw sink, was normalized, and then weighted by VexLens as if signed.
Two verifier surfaces existed in parallel:
- V1
IVexSignatureVerifier(legacy;nullreturn = “no metadata, ingest anyway”). - V2
IVexSignatureVerifierV2(ProductionVexSignatureVerifier) wired into DI but never invoked byVexConnectorBase.
Decision
Verification is a mandatory ingest gate. Connectors MUST produce raw documents through
VexConnectorBase.CreateRawDocumentAsync(context, format, sourceUri, content, metadata, verificationOptions, ct), which performs verification before the digest is computed and the document is handed back. The synchronousCreateRawDocument(...)overload is[Obsolete(DiagnosticId="EXCITITOR-VRF-01")].StellaOps.Cryptographyis the only verification primitive. All signature verification is delegated toICryptoProviderRegistry. The existingProductionVexSignatureVerifieris the implementation; no new crypto primitives are introduced. This honours the regional crypto plugin contract (FIPS / GOST / SM / eIDAS).The V1 surface becomes a fail-closed adapter. The legacy
VerificationDisabledVexSignatureVerifieris deleted.IVexSignatureVerifieris satisfied exclusively byStellaOps.Excititor.Core.Verification.VexSignatureVerifierV1Adapter, which routes every call to V2 and throwsVexSignatureRequiredExceptionon missing/invalid signatures. The V1 contract returningnullis reserved for advisory “unsigned, decide at the connector boundary” cases.AllowUnsignedis non-default and forbidden in production. Per-fetchVexConnectorVerificationOptions.AllowUnsigned == trueadmits unsigned documents and stampsvex.signature.status=unsignedmetadata, but emits a Warning log entry and is rejected by a hosted startup validator (VexConnectorVerificationStartupValidator) whenIHostEnvironment.IsProduction()is true and the globalVexSignatureVerification:Connectors:AllowUnsignedknob is set.Stable metadata keys for the AOC guard. The verifier’s success metadata is merged into the document’s
Metadatausing theVexSignatureMetadataKeysconstants (vex.signature.status,vex.signature.issuer,vex.signature.issuerId,vex.signature.keyId,vex.signature.method,vex.signature.profile,vex.signature.verifiedAt,vex.signature.transparency.entry,vex.signature.transparency.logId). Sprint 20260501-023 reads these keys in the AOC pre-normalize guard.
Consequences
Positive
- A tampered or unsigned CSAF document can no longer reach the raw sink in a production deployment without an explicit operator opt-in that the host refuses to start with.
- All region-specific crypto regulation (FIPS / GOST / SM) flows through
ICryptoProviderRegistryrather than fragmented per-connector primitives. - The V1 surface, which is still used by tests and a handful of CSAF connectors, now behaves identically to V2 from the connector’s point of view.
Negative / mitigations
- Breaking change for test fixtures registering
VerificationDisabledVexSignatureVerifierdirectly. Mitigation: use aFakeVexSignatureVerifiertest double that returns null for unsigned and optionally returns aVexSignatureMetadatafor trusted digests. See the OCI connector tests for the canonical pattern after migration. - CSAF connectors not migrated in this sprint. The synchronous
CreateRawDocumentoverload is[Obsolete](warning, not error) pending sprint 20260501-021 which threadsVexConnectorContextthrough CSAF helpers and migrates each connector toCreateRawDocumentAsync. CSAF connectorcsprojfiles temporarily set<NoWarn>$(NoWarn);EXCITITOR-VRF-01</NoWarn>. The promotion toerror: truelands at the end of sprint 021. ITenantContextAccessorfor scheduled-job runs. The V1 adapter extracts the tenant fromVexRawDocument.Metadata(tenant-id/X-StellaOps-TenantId) and falls back to@global. Worker-side scheduled jobs MUST set the tenant header on the document or accept the@globalfallback. Sprint 20260501-021 makes this explicit per connector.
Out of scope (handed off)
- Quarantine sink + 30-day retention table. EXCITITOR-VRF-03 was scoped for this sprint but requires a Postgres EF migration in
StellaOps.Excititor.Storage.Postgres, which is outside the Abstractions/Core working directory. Tracked separately as the remaining BLOCKED task on sprint 20260501-020. - Worker config sample (
etc/excititor.worker.yaml). Sprint 20260501-020 expected this file; deferred until the operator-facing config schema for the new options stabilises with sprint 021’s per-connector settings.
Compliance
- BUSL-1.1: no new third-party dependencies; reuses already-licensed
StellaOps.Cryptographyprimitives. - Determinism: all behaviour is deterministic given a fixed verifier and fixed input bytes. The metadata stamps use stable keys (no UUIDs, no ambient time except
vex.signature.verifiedAtwhich is supplied by the verifier). - Offline-first: the verifier registry resolves providers locally; no external HTTP fetches are introduced by this ADR.
