# 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 |