EU Offline Verifier Trust Roots
Status: Active Signer security contract Owner: Attestor/Signer guild Sprint: docs-archive/implplan/SPRINT_20260430_203_Signer_eu_signing_verifier_contracts.md
This page is the security contract for the Stella Ops EU offline artefact verifier (EuArtifactOfflineVerifier): how trust roots are shaped, normalized, and time-validated when regulators or auditors replay signed EU artefacts entirely offline. It is written for Signer/Attestor engineers and for operators who assemble trust bundles for sealed and air-gapped deployments. Verification never touches the network — it validates only local envelope bytes, local trust roots, local schema-pin data, and caller-supplied replay metadata.
See also: offline-verification-crypto-provider.md for the underlying verification provider and crypto-compliance.md for the eidas compliance profile.
Trust Root Shape
Each EU verifier trust root is explicit local metadata:
{
"trustRootId": "tenant-reporting-root-2026",
"keyId": "tenant-reporting-key",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----...",
"fingerprint": "sha256:<64 lowercase hex>",
"notBeforeUtc": "2026-01-01T00:00:00Z",
"notAfterUtc": "2027-01-01T00:00:00Z",
"signerProfiles": ["tenant-regulatory-reporting"]
}
The fingerprint is the sha256:-prefixed lowercase-hex digest over the DER SubjectPublicKeyInfo bytes (computed by EuArtifactOfflineVerifier.ComputePublicKeyFingerprint, which delegates to CanonJson.Sha256Prefixed). A -----BEGIN CERTIFICATE----- PEM is accepted: the verifier extracts the certificate’s public key and computes the fingerprint over the extracted SPKI, not over the certificate bytes.
EuArtifactOfflineVerifier.NormalizeTrustRootsAsync silently drops a candidate root from the verifier set when any of the following hold:
trustRootIdorkeyIdis blank.notAfterUtc <= notBeforeUtc(an empty or inverted validity window).signerProfilesis empty.- the PEM is malformed, or carries a
PRIVATE KEY/EC PRIVATE KEY/RSA PRIVATE KEYblock (private key material is never accepted as a trust root). - the supplied
fingerprintdoes not exactly match the recomputed fingerprint.
If every supplied root is dropped (or none are supplied), the result fails with trust_roots_required. Dropped roots are not surfaced as individual error codes; they simply leave no candidate able to verify the signature, yielding signature_untrusted:<keyId> and no_valid_signature.
Loading Modes
EuOfflineVerificationRequest.TrustRootMode is an EuTrustRootMode enum with three members (Online, Sealed, AirGap). The field is required on the request record but currently informational only: EuArtifactOfflineVerifier does not branch on it, and verification is always offline regardless of the value. The mode is therefore evidence metadata recording how the operator sourced the trust roots, not a switch that changes verifier behaviour or grants permission to fetch keys.
| Mode | Expected source of trust roots | Expected use |
|---|---|---|
Online | Operator-managed runtime configuration projected into the Signer/CLI process | Connected administrative environments |
Sealed | Sealed bootstrap bundle or sealed environment config | Locked-down deployments with mounted config |
AirGap | Offline Kit or physical-media trust bundle | Disconnected review and regulator evidence replay |
All modes use the same verifier contract. None of them cause the verifier to perform network access: it validates only local envelope bytes, local trust roots, local schema-pin data, and caller-supplied replay metadata.
Validity Rules
- A trust root is usable when
notBeforeUtc <= verificationTimeUtc <= notAfterUtc(VerificationTimeUtcis supplied on the request). - Roots outside that window fail closed with
trust_root_not_valid_at_time:<trustRootId>. - Historical verification may use an out-of-window root only when
AllowHistoricalVerification=trueand the optionalSignedAtUtctimestamp is itself inside the root validity window (notBeforeUtc <= signedAtUtc <= notAfterUtc). In that branch the currentVerificationTimeUtcis no longer required to fall inside the window. - The trust root signer profile must include the payload registry entry’s
ExpectedSignerProfile; otherwise the candidate is rejected withsigner_profile_not_allowed:<keyId>. For example, a root whose only profile istenant-regulatory-reportingcannot verify aproduct-csaf-advisory-v1payload, whose expected profile isstella-manufacturer-release. - Any expired or superseded signing key (for example a retired release key) may be recorded as lineage in external operator documentation, but it must not be loaded as a live trust root once its validity window has closed. This is an operational convention; the verifier itself only enforces the validity window and signer-profile checks above. (No “website release key” lineage concept is modelled in the EU signing code — see flagged note.)
Operational Notes
- Ship public keys and fingerprints in offline bundles; never ship private key material.
- Keep trust root ids stable and versioned so audit events can reference the exact root used for verification.
- Rotate roots with overlap. During overlap, both current and next roots may be valid, but each root still needs its own explicit validity window.
- Verification failure reasons are deterministic.
EuSigningAuditLinkageBuilder.FromVerificationprojects the request/result pair into anEuSigningAuditLinkagerecord under theevidence.eu.artifact.verifiedaudit event, carrying the trust root id, trust root fingerprint, verifier result (valid/invalid), and a deduplicated, ordinal-sorted issue list. The Signer verifier is a local/offline contract and does not write EvidenceLocker tables directly; downstream sign/export flows record these fields in EvidenceLocker’s regulatory audit events (evidence_locker.regulatory_audit_events) and artifact ledger (evidence_locker.regulatory_artifact_ledger).
