# Staleness & Time Anchor Contract v1.0.0 **Status:** APPROVED **Version:** 1.0.0 **Effective:** 2025-12-19 **Owner:** AirGap Guild + Findings Ledger Guild **Sprint:** SPRINT_0510_0001_0001 (unblocks LEDGER-AIRGAP-56-002, LEDGER-AIRGAP-57-001) --- ## 1. Purpose This contract defines how air-gapped StellaOps installations maintain trusted time references, calculate data staleness, and enforce freshness policies. It enables deterministic vulnerability triage even when disconnected from external time sources. ## 2. Schema References | Schema | Location | |--------|----------| | Time Anchor | `docs/schemas/time-anchor.schema.json` | | Ledger Staleness | `docs/schemas/ledger-airgap-staleness.schema.json` | | Sealed Mode | `docs/schemas/sealed-mode.schema.json` | ## 3. Architecture ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Air-Gapped Environment │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ │ Mirror │───▶│ AirGap │───▶│ AirGap Time │ │ │ │ Bundle │ │ Controller │ │ Service │ │ │ │ (time anchor)│ └──────────────┘ └──────────────────────┘ │ │ └──────────────┘ │ │ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────────────────────┐ │ │ │ Staleness Calculator │ │ │ │ (drift, budgets, validation) │ │ │ └──────────────────────────────────────────┘ │ │ │ │ │ │ ┌─────────────┴─────────────────────┴───────────┐ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ Findings Ledger │ │ Policy Engine │ │ │ │ (staleness tracking) │ │ (evaluation gating) │ │ │ └──────────────────────┘ └──────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ## 4. Core Types ### 4.1 TimeAnchor A cryptographically signed time reference: ```csharp public sealed record TimeAnchor { /// RFC 3339 timestamp of the anchor. public required DateTimeOffset AnchorTime { get; init; } /// Source of the time anchor. public required TimeSource Source { get; init; } /// Format identifier (roughtime-v1, rfc3161-v1). public required string Format { get; init; } /// SHA-256 digest of the time token. public required string TokenDigest { get; init; } /// Signing key fingerprint. public string? SignatureFingerprint { get; init; } /// Verification status. public VerificationStatus? Verification { get; init; } /// Monotonic counter for replay protection. public long? MonotonicCounter { get; init; } } public enum TimeSource { Roughtime = 0, Rfc3161 = 1, HardwareClock = 2, AttestationTsa = 3, Manual = 4, Unknown = 5 } public sealed record VerificationStatus { public required VerificationState Status { get; init; } public string? Reason { get; init; } public DateTimeOffset? VerifiedAt { get; init; } } public enum VerificationState { Unknown = 0, Passed = 1, Failed = 2 } ``` ### 4.2 StalenessBudget Configuration for acceptable data freshness: ```csharp public sealed record StalenessBudget { /// Budget identifier. public required string BudgetId { get; init; } /// Domain this budget applies to. public required string DomainId { get; init; } /// Maximum staleness in seconds before data is stale. public required TimeSpan FreshnessThreshold { get; init; } /// Warning threshold (percentage of freshness threshold). public decimal WarningThresholdPercent { get; init; } = 75m; /// Critical threshold (percentage of freshness threshold). public decimal CriticalThresholdPercent { get; init; } = 90m; /// Grace period after threshold before hard enforcement. public TimeSpan GracePeriod { get; init; } = TimeSpan.FromDays(1); /// Enforcement mode. public EnforcementMode EnforcementMode { get; init; } = EnforcementMode.Strict; } public enum EnforcementMode { Strict = 0, // Block operations when stale Warn = 1, // Allow but log warnings Disabled = 2 // No enforcement } ``` ### 4.3 StalenessEvaluation Result of staleness calculation: ```csharp public sealed record StalenessEvaluation { /// Domain evaluated. public required string DomainId { get; init; } /// Current staleness duration. public required TimeSpan CurrentStaleness { get; init; } /// Configured threshold. public required TimeSpan Threshold { get; init; } /// Staleness as percentage of threshold. public required decimal PercentOfThreshold { get; init; } /// Overall status. public required StalenessStatus Status { get; init; } /// When data will become stale. public DateTimeOffset? ProjectedStaleAt { get; init; } /// Time anchor used for calculation. public required TimeAnchor TimeAnchor { get; init; } /// Last bundle import timestamp. public required DateTimeOffset LastImportAt { get; init; } /// Source timestamp of last bundle. public required DateTimeOffset LastSourceTimestamp { get; init; } } public enum StalenessStatus { Fresh = 0, // < warning threshold Warning = 1, // >= warning, < critical Critical = 2, // >= critical, < threshold Stale = 3, // >= threshold, < threshold + grace Breached = 4 // >= threshold + grace } ``` ### 4.4 BundleProvenance Provenance record for imported bundles: ```csharp public sealed record BundleProvenance { /// Unique bundle identifier. public required Guid BundleId { get; init; } /// Bundle domain (vex-advisories, vulnerability-feeds, etc.). public required string DomainId { get; init; } /// When bundle was imported. public required DateTimeOffset ImportedAt { get; init; } /// Original generation timestamp from source. public required DateTimeOffset SourceTimestamp { get; init; } /// Source environment identifier. public string? SourceEnvironment { get; init; } /// SHA-256 digest of bundle contents. public required string BundleDigest { get; init; } /// SHA-256 digest of bundle manifest. public string? ManifestDigest { get; init; } /// Staleness at import time. public required TimeSpan StalenessAtImport { get; init; } /// Time anchor used for staleness calculation. public required TimeAnchor TimeAnchor { get; init; } /// DSSE attestation covering this bundle. public BundleAttestation? Attestation { get; init; } /// Exports included in this bundle. public ImmutableArray Exports { get; init; } } ``` ## 5. Staleness Domains | Domain ID | Description | Default Threshold | Default Grace | |-----------|-------------|-------------------|---------------| | `vulnerability-feeds` | Advisory and CVE data | 7 days | 1 day | | `vex-advisories` | VEX statements | 7 days | 1 day | | `scanner-signatures` | Scanner detection rules | 14 days | 3 days | | `policy-packs` | Policy bundles | 30 days | 7 days | | `trust-roots` | Certificate/key roots | 90 days | 14 days | | `runtime-evidence` | Runtime observation data | 1 day | 4 hours | ## 6. Time Anchor Verification ### 6.1 Roughtime Verification ```csharp public interface IRoughtimeVerifier { /// /// Verifies a Roughtime response against trusted servers. /// Task VerifyAsync( byte[] roughtimeResponse, RoughtimeRoot[] trustedRoots, CancellationToken cancellationToken = default); } ``` Roughtime provides: - Sub-second accuracy with 1-2 second uncertainty - Ed25519 signatures - Chain of trust via server public keys - Radius-based uncertainty bounds ### 6.2 RFC 3161 Verification ```csharp public interface IRfc3161Verifier { /// /// Verifies an RFC 3161 timestamp token. /// Task VerifyAsync( byte[] timestampToken, Rfc3161Root[] trustedRoots, CancellationToken cancellationToken = default); } ``` RFC 3161 provides: - X.509 certificate-based trust - ASN.1/DER encoded tokens - Hash algorithm binding - Nonce for uniqueness ### 6.3 Validation Result ```csharp public sealed record TimeAnchorValidationResult { public required bool IsValid { get; init; } public required TimeAnchor? Anchor { get; init; } public TimeAnchorError? Error { get; init; } public TimeSpan? Uncertainty { get; init; } } public enum TimeAnchorError { None = 0, SignatureInvalid = 1, RootNotTrusted = 2, TokenExpired = 3, TokenMalformed = 4, CounterReplay = 5, UncertaintyTooHigh = 6 } ``` ## 7. API Endpoints ### 7.1 AirGap Time Service | Endpoint | Method | Description | |----------|--------|-------------| | `GET /api/v1/time/status` | GET | Current anchor metadata and drift | | `GET /api/v1/time/anchor` | GET | Active time anchor | | `POST /api/v1/time/anchor` | POST | Import new time anchor | | `GET /api/v1/time/metrics` | GET | Prometheus metrics | | `GET /api/v1/time/health` | GET | Health check | ### 7.2 Staleness Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `GET /api/v1/staleness/domains` | GET | List all domain staleness | | `GET /api/v1/staleness/domains/{domainId}` | GET | Get domain staleness | | `POST /api/v1/staleness/validate` | POST | Validate staleness for context | | `GET /api/v1/staleness/config` | GET | Get staleness configuration | | `PUT /api/v1/staleness/config` | PUT | Update staleness configuration | ### 7.3 Response Formats ```json { "domainId": "vex-advisories", "currentStaleness": "PT172800S", "threshold": "PT604800S", "percentOfThreshold": 28.57, "status": "fresh", "projectedStaleAt": "2025-12-26T10:00:00Z", "timeAnchor": { "anchorTime": "2025-12-19T10:00:00Z", "source": "roughtime", "format": "roughtime-v1", "tokenDigest": "sha256:abc123...", "verification": { "status": "passed", "verifiedAt": "2025-12-19T10:00:01Z" } }, "lastImportAt": "2025-12-17T10:00:00Z", "lastSourceTimestamp": "2025-12-17T08:00:00Z" } ``` ## 8. Integration Points ### 8.1 Findings Ledger Integration The Ledger tracks staleness per projection: ```csharp public interface IStalenessValidationService { /// /// Validates that data is fresh enough for the given context. /// Task ValidateAsync( string tenantId, string domainId, StalenessContext context, CancellationToken cancellationToken = default); /// /// Updates staleness tracking after bundle import. /// Task UpdateStalenessAsync( string tenantId, BundleProvenance provenance, CancellationToken cancellationToken = default); } public enum StalenessContext { Export = 0, // Generating exports Query = 1, // Querying data PolicyEval = 2, // Policy evaluation Attestation = 3 // Creating attestations } ``` ### 8.2 Policy Engine Integration Policy Engine gates evaluations based on staleness: ```csharp public interface ISealedModeService { /// /// Checks if sealed mode should block the operation. /// Task CheckAsync( string tenantId, SealedModeContext context, CancellationToken cancellationToken = default); } public sealed record SealedModeDecision { public required bool IsBlocked { get; init; } public SealedModeReason? Reason { get; init; } public ImmutableArray StaleDomains { get; init; } } public enum SealedModeReason { None = 0, DataStale = 1, TimeAnchorMissing = 2, TimeAnchorExpired = 3, SignatureInvalid = 4 } ``` ## 9. Telemetry ### 9.1 Metrics | Metric | Type | Labels | Description | |--------|------|--------|-------------| | `airgap_anchor_age_seconds` | gauge | - | Age of current time anchor | | `airgap_anchor_drift_seconds` | gauge | - | Drift from anchor time | | `airgap_anchor_expiry_seconds` | gauge | - | Seconds until anchor expires | | `airgap_staleness_seconds` | gauge | `domain` | Current staleness per domain | | `airgap_staleness_threshold_seconds` | gauge | `domain` | Threshold per domain | | `airgap_staleness_percent` | gauge | `domain` | Staleness as % of threshold | | `airgap_staleness_status` | gauge | `domain`, `status` | Current status (0=fresh, 3=stale) | | `airgap_bundle_imports_total` | counter | `domain`, `result` | Bundle imports | | `airgap_validation_total` | counter | `domain`, `context`, `result` | Staleness validations | ### 9.2 Alerts ```yaml # Recommended alerting rules groups: - name: airgap-staleness rules: - alert: AirGapDataApproachingStale expr: airgap_staleness_percent > 75 for: 1h labels: severity: warning annotations: summary: "{{ $labels.domain }} data approaching staleness" - alert: AirGapDataStale expr: airgap_staleness_percent >= 100 for: 5m labels: severity: critical annotations: summary: "{{ $labels.domain }} data is stale" - alert: AirGapTimeAnchorMissing expr: airgap_anchor_age_seconds > 86400 for: 5m labels: severity: critical annotations: summary: "Time anchor is older than 24 hours" ``` ## 10. Configuration ```yaml # etc/airgap.yaml AirGap: Time: Enabled: true TrustRootsPath: "/etc/stellaops/trust-roots.json" MaxAnchorAgeHours: 168 # 7 days MaxUncertaintyMs: 5000 # 5 seconds Staleness: DefaultThresholdDays: 7 DefaultGracePeriodDays: 1 EnforcementMode: "Strict" # Strict, Warn, Disabled Domains: vulnerability-feeds: ThresholdDays: 7 GracePeriodDays: 1 vex-advisories: ThresholdDays: 7 GracePeriodDays: 1 runtime-evidence: ThresholdDays: 1 GracePeriodHours: 4 Notifications: - PercentOfThreshold: 75 Severity: warning Channels: [slack, metric] - PercentOfThreshold: 90 Severity: critical Channels: [email, slack, metric] ``` ## 11. Error Codes | Code | Description | Resolution | |------|-------------|------------| | `ERR_AIRGAP_STALE` | Data exceeds staleness threshold | Import fresh bundle | | `ERR_AIRGAP_NO_BUNDLE` | No bundle imported for domain | Import initial bundle | | `ERR_AIRGAP_TIME_ANCHOR_MISSING` | No time anchor available | Import time anchor with bundle | | `ERR_AIRGAP_TIME_DRIFT` | Excessive drift detected | Re-verify time anchor | | `ERR_AIRGAP_ATTESTATION_INVALID` | Bundle attestation invalid | Verify bundle source | | `ERR_AIRGAP_SIGNATURE_INVALID` | Time token signature invalid | Check trust roots | | `ERR_AIRGAP_COUNTER_REPLAY` | Monotonic counter replay | Import newer anchor | --- ## Changelog | Version | Date | Changes | |---------|------|---------| | 1.0.0 | 2025-12-19 | Initial release |