Skip to main content

ADR-008: Offline Grace Period Implementation

Status: Accepted Date: 2025-11-30 Deciders: Architecture Team, Product Team, Security Team Tags: licensing, offline, security, user-experience, cryptography


Table of Contents

  1. Context
  2. Decision
  3. Consequences
  4. Alternatives Considered
  5. Implementation Notes
  6. Technical Implementation
  7. Security Considerations
  8. Testing Strategy
  9. Monitoring and Observability
  10. Related ADRs
  11. References

Context

Business Problem

CODITECT developers frequently work in offline environments where internet connectivity is unavailable or unreliable:

Common Offline Scenarios:

  • ✈️ Air travel - Developers working on flights (2-12 hours offline)
  • Coffee shops/co-working - Unreliable WiFi with frequent dropouts
  • 🏢 Air-gapped environments - Government, financial, healthcare sectors with isolated networks
  • 🌍 Remote locations - Developing countries with intermittent connectivity
  • 🚂 Commuting - Train/subway development with no network access
  • 🏠 Home internet outages - ISP issues preventing connectivity

User Experience Requirements:

Developer workflow (Monday flight):
06:00 - Developer wakes up, starts CODITECT at home (license acquired)
07:00 - Heads to airport (offline for next 8 hours)
07:30 - Boards plane, opens laptop
08:00 - Starts coding CODITECT session (should work offline)
12:00 - Still coding on flight (4 hours offline)
16:00 - Lands, connects to airport WiFi (license renewed)

Expected: ✅ Full CODITECT functionality for entire flight
Unacceptable: ❌ License validation failure after 1 hour offline

Technical Constraints

CODITECT Architecture (Local-First):

  • Framework installed as git submodule (.coditect/)
  • Runs entirely on user's machine (no cloud dependency for execution)
  • License server in cloud ONLY validates licenses and tracks seats
  • Offline validation must not compromise security

Security Requirements:

  • Tamper-proof offline licenses - Prevent license key sharing
  • Time-bounded validity - Prevent unlimited offline usage
  • Clock manipulation detection - Prevent system time tampering
  • Revocation support - Invalidate licenses even when offline

User Experience Goals:

  • Transparent operation - Developers shouldn't notice offline mode
  • Clear warnings - Notifications before grace period expiration
  • Predictable behavior - Same functionality online and offline
  • Fair pricing - Offline grace tied to tier value (Free < Pro < Enterprise)

Floating License Context

From ADR-001: Floating Licenses, we have tier-based concurrent seats:

TierSeatsPrice/MonthOffline Grace Needed
Free1$024 hours (minimal)
Pro3$8772 hours (3 days)
Team5$14548 hours (2 days)
EnterpriseUnlimited$870+168 hours (7 days)

Rationale for Tier-Based Grace:

  • Free tier (24h) - Prevent abuse while supporting daily commutes
  • Pro tier (72h) - Support weekend projects and short trips
  • Team tier (48h) - Balance between Pro and Enterprise
  • Enterprise tier (168h) - Full week for business travel and air-gapped deployments

Decision

We will implement tier-based offline grace periods with Cloud KMS-signed license caching, tamper detection, and clock skew verification.

High-Level Architecture

Online License Acquisition:

1. Developer starts CODITECT (init.sh)
└─ Check internet connectivity

2. License API validates credentials
├─ Check JWT token (Identity Platform)
├─ Atomic seat check (Redis Lua script)
└─ Calculate offline expiration timestamp

3. Cloud KMS signs license payload
├─ RSA-4096 asymmetric key
├─ Includes offline_expires_at timestamp
└─ Returns signature + public key ID

4. License cached locally (.coditect/.license-cache)
├─ JSON with signature
├─ Tamper-proof via Cloud KMS signature
└─ Includes tier-specific offline grace period

5. Developer works offline
├─ Cached license validated locally
├─ Signature verified with Cloud KMS public key
├─ Timestamp checked against offline_expires_at
└─ Warnings displayed when <24h remaining

6. Online reconnection
└─ License renewed with new offline_expires_at

Offline Grace Periods by Tier

# Production-ready grace period configuration
OFFLINE_GRACE_HOURS = {
'free': 24, # 1 day - basic offline support
'pro': 72, # 3 days - weekend + short trips
'team': 48, # 2 days - business use
'enterprise': 168 # 7 days - air-gapped + business travel
}

Tier Selection Rationale:

TierGrace PeriodUse CasesAbuse Prevention
Free24 hoursDaily commutes, coffee shop workShort enough to require frequent online checks
Pro72 hoursWeekend projects, 3-day tripsCovers typical short business trips
Team48 hours2-day sprints, client visitsShorter than Pro to incentivize upgrades
Enterprise168 hoursFull work week, air-gapped deploymentsLong enough for business travel + security clearance delays

Warning System Thresholds

# Progressive warnings before grace expiration
WARNING_THRESHOLDS = {
'first_warning': 24, # 24 hours remaining
'second_warning': 12, # 12 hours remaining
'final_warning': 6, # 6 hours remaining
'critical_warning': 1 # 1 hour remaining
}

User Experience:

# 24 hours before expiration
⚠️ Offline license expires in 24 hours. Connect to internet soon to renew.

# 12 hours before expiration
⚠️ IMPORTANT: Offline license expires in 12 hours.
Please connect to the internet to avoid losing access.

# 6 hours before expiration
⚠️ URGENT: Offline license expires in 6 hours!
CODITECT will stop working without internet connection.
Save your work and connect to renew your license.

# 1 hour before expiration
🚨 CRITICAL: Offline license expires in 1 hour!
IMMEDIATE ACTION REQUIRED: Connect to internet NOW.

# Grace period expired
❌ Offline license expired. Internet connection required.
Please connect to the internet to renew your license.

Consequences

Positive

Offline Development Support

  • Developers can work on flights, trains, and remote locations
  • Grace periods cover 95%+ of legitimate offline scenarios
  • Tier-based grace aligns with pricing and user needs

Tamper-Proof Security

  • Cloud KMS RSA-4096 signatures prevent license tampering
  • Signature verification happens client-side (offline-capable)
  • No client-side secret keys (only public key distribution)

Fair Pricing Model

  • Longer grace periods for higher-paying tiers
  • Free tier limited to 24h prevents unlimited offline abuse
  • Enterprise tier supports air-gapped and high-security environments

Transparent User Experience

  • Seamless online/offline transitions
  • Progressive warnings prevent surprise lockouts
  • Clear error messages with actionable steps

Revocation Support

  • Expired grace periods force online re-validation
  • Allows license revocation even after offline usage
  • Prevents indefinite offline usage via time limits

Negative

⚠️ Clock Manipulation Risk

  • Users could set system clock backwards to extend grace period
  • Mitigation: NTP clock skew detection (flags if >5 minutes skew)
  • Mitigation: Signed timestamps prevent backward time travel
  • Limitation: Cannot fully prevent determined attackers with root access

⚠️ Air-Gapped Environment Limitations

  • Enterprise tier 168h grace may be insufficient for some air-gapped deployments
  • Workaround: Manual license renewal process for special cases
  • Consideration: Future node-locked license option for permanent air-gap

⚠️ Grace Period Expiration Lockout

  • Developers working offline >grace period will be locked out
  • Mitigation: Email alerts 24h before expiration
  • Mitigation: SMS alerts for Enterprise tier (optional)
  • User education: Check grace period before offline travel

⚠️ Storage Overhead

  • Cached licenses stored on disk (.coditect/.license-cache)
  • Includes signature (~512 bytes RSA-4096) + metadata (~2KB)
  • Negligible impact: <3KB per cached license

Neutral

🔄 Offline-First Validation Flow

  • CODITECT checks cached license before online API call
  • Reduces API latency (cached validation <1ms vs 50-100ms API call)
  • Online validation only when cache invalid or expired

🔄 Grace Period as Competitive Differentiator

  • Longer grace periods can justify higher tier pricing
  • May attract developers in low-connectivity regions
  • Potential upsell opportunity (Pro → Enterprise for longer grace)

Alternatives Considered

Alternative 1: Always-Online Licensing (No Offline Support)

Implementation:

# Every CODITECT startup requires internet
def validate_license():
try:
response = requests.post(
f"{LICENSE_API}/validate",
json={'license_key': license_key},
timeout=10
)
return response.status_code == 200
except requests.exceptions.RequestException:
return False # FAIL - No offline support

Pros:

  • ✅ Simplest implementation (no caching, no signatures)
  • ✅ Perfect license revocation (immediate effect)
  • ✅ No clock manipulation risk
  • ✅ No grace period abuse

Cons:

  • ❌ Unusable on flights, trains, remote locations
  • ❌ Poor developer experience (internet dependency)
  • ❌ Competitive disadvantage vs. JetBrains, VS Code (offline support)
  • ❌ Excludes government/healthcare air-gapped customers

Rejected Because: Unacceptable user experience for modern development workflows. Developers expect offline capabilities.

Alternative 2: Unlimited Offline (No Expiration)

Implementation:

# License never expires offline
def validate_cached_license(cached_license):
# Only check signature, no timestamp validation
if verify_signature(cached_license):
return True # Valid forever
return False

Pros:

  • ✅ Best offline developer experience
  • ✅ No grace period management complexity
  • ✅ No clock skew detection needed
  • ✅ Works in permanent air-gap environments

Cons:

  • ❌ Revenue leakage (unlimited sharing via offline licenses)
  • ❌ No license revocation capability
  • ❌ License key shared across unlimited users
  • ❌ Cannot enforce subscription expiration

Rejected Because: Unsustainable business model. Cannot prevent unlimited license sharing.

Alternative 3: Hardware-Locked Offline Licenses

Implementation:

# Offline license tied to hardware fingerprint
def generate_offline_license(license_key, hardware_id):
payload = {
'license_key': license_key,
'hardware_id': hardware_id, # MAC + CPU + disk UUID
'offline_valid_until': datetime.max # Permanent
}
signature = cloud_kms_sign(payload)
return {'payload': payload, 'signature': signature}

Pros:

  • ✅ Prevents license sharing (tied to specific hardware)
  • ✅ Supports permanent air-gap environments
  • ✅ No expiration management complexity

Cons:

  • ❌ Poor developer experience (hardware changes require re-activation)
  • ❌ Laptop upgrade/replacement breaks license
  • ❌ VM migration breaks license
  • ❌ Doesn't align with floating license model (ADR-001)

Rejected Because: Conflicts with floating license architecture. Too restrictive for modern workflows (developers use multiple machines).

Alternative 4: Blockchain-Based Time Verification

Implementation:

# Use blockchain timestamps as trusted time source
import hashlib
from bitcoinrpc.authproxy import AuthServiceProxy

def get_blockchain_time():
"""Query Bitcoin blockchain for trusted timestamp"""
bitcoin_client = AuthServiceProxy("http://user:pass@localhost:8332")
latest_block = bitcoin_client.getbestblockhash()
block_info = bitcoin_client.getblock(latest_block)
return block_info['time'] # Trusted timestamp

def validate_offline_license(cached_license):
blockchain_time = get_blockchain_time() # Requires internet!
offline_expires_at = cached_license['offline_expires_at']

if blockchain_time < offline_expires_at:
return True
return False

Pros:

  • ✅ Tamper-proof time source (cannot fake blockchain timestamps)
  • ✅ Decentralized trust (no single authority)
  • ✅ Prevents clock manipulation completely

Cons:

  • ❌ Requires internet connection (defeats purpose of offline support!)
  • ❌ Blockchain query latency (5-10 seconds)
  • ❌ Complexity: Bitcoin node setup, RPC configuration
  • ❌ Operational overhead: Maintain blockchain node
  • ❌ Overkill for the problem (NTP simpler and sufficient)

Rejected Because: Too complex for marginal benefit. Requires internet connection, defeating the purpose of offline validation.

Alternative 5: Trusted Platform Module (TPM) Time Attestation

Implementation:

# Use TPM secure clock for tamper-proof timestamps
import tpm2_pytss

def get_tpm_time():
"""Read secure time from TPM chip"""
with tpm2_pytss.ESAPI() as ectx:
time_info = ectx.read_clock()
return time_info.time # Hardware-protected timestamp

def validate_offline_license(cached_license):
tpm_time = get_tpm_time()
offline_expires_at = cached_license['offline_expires_at']

if tpm_time < offline_expires_at:
return True
return False

Pros:

  • ✅ Hardware-protected time source (cannot be tampered)
  • ✅ Works completely offline
  • ✅ Industry standard (FIPS 140-2 compliant)

Cons:

  • ❌ TPM not available on all systems (macOS, older hardware)
  • ❌ Requires root/admin access to read TPM
  • ❌ Complex setup (tpm2-tools, kernel modules)
  • ❌ TPM clock can be reset by motherboard battery removal
  • ❌ Poor cross-platform support

Rejected Because: Limited platform support and user experience issues. Not all developers have TPM-enabled systems.

Alternative 6: Hybrid Model (Short Grace + Extended Vouchers)

Implementation:

# Base grace period (24h) + vouchers for extended offline
OFFLINE_GRACE_HOURS = {
'all_tiers': 24 # Base grace for everyone
}

# Enterprise customers get vouchers for extended offline
def request_offline_voucher(license_key, duration_days):
"""Request extended offline voucher (Enterprise only)"""
response = requests.post(
f"{LICENSE_API}/offline-voucher",
json={
'license_key': license_key,
'duration_days': duration_days, # 7, 14, 30 days
'reason': 'Business travel to air-gapped site'
}
)
voucher = response.json()
# Voucher signed by Cloud KMS, extends grace period
cache_offline_voucher(voucher)

Pros:

  • ✅ Flexible offline support (on-demand extensions)
  • ✅ Audit trail (voucher requests logged)
  • ✅ Prevents blanket long grace periods
  • ✅ Better revocation control (vouchers can be denied)

Cons:

  • ❌ Requires pre-planning (developer must request voucher before offline)
  • ❌ Poor user experience (manual voucher request process)
  • ❌ Complexity: Two-tier license validation (base + voucher)
  • ❌ Developer friction (forgot to request voucher = locked out)

Rejected Because: Too complex for v1.0. Consider for v2.0 based on customer feedback. Current tier-based grace covers 95%+ of use cases.


Implementation Notes

License Cache Structure

File: .coditect/.license-cache (JSON)

{
"license_key": "CODITECT-TEAM-XXXX-YYYY-ZZZZ",
"tier": "team",
"features": [
"all_agents",
"all_commands",
"floating_seats"
],
"user_email": "developer@company.com",
"acquired_at": "2025-11-30T12:00:00Z",
"expires_at": "2026-11-30T12:00:00Z",
"offline_expires_at": "2025-12-02T12:00:00Z",
"session_id": "abc123def456...",
"hardware_id": "e3b0c44298fc1c149afbf4c8996fb924",
"signature": "base64_encoded_rsa_4096_signature_512_bytes...",
"public_key_id": "projects/coditect-cloud-infra/locations/us-central1/keyRings/license-keys/cryptoKeys/license-signing-key/cryptoKeyVersions/1",
"ntp_timestamp": "2025-11-30T12:00:03Z",
"ntp_source": "pool.ntp.org"
}

Field Descriptions:

FieldTypeDescriptionRequired
license_keystringLicense key (CODITECT-{TIER}-{UUID})Yes
tierstringTier level (free, pro, team, enterprise)Yes
featuresarrayEnabled features for this tierYes
user_emailstringUser email from git configYes
acquired_atISO8601License acquisition timestamp (UTC)Yes
expires_atISO8601License subscription expirationYes
offline_expires_atISO8601Offline grace period expirationYes
session_idstringUnique session ID (prevents reuse)Yes
hardware_idstringHardware fingerprint (SHA256)Yes
signaturestringCloud KMS RSA-4096 signature (base64)Yes
public_key_idstringCloud KMS public key resource IDYes
ntp_timestampISO8601NTP server timestamp (for clock skew)Optional
ntp_sourcestringNTP server used (e.g., pool.ntp.org)Optional

Signature Coverage:

The Cloud KMS signature covers these fields (canonicalized JSON):

{
"license_key": "...",
"tier": "...",
"acquired_at": "...",
"expires_at": "...",
"offline_expires_at": "...",
"session_id": "...",
"hardware_id": "..."
}

Why These Fields?

  • license_key, tier, expires_at - Core license identity
  • offline_expires_at - Prevents extending grace period
  • session_id - Prevents license cache reuse across sessions
  • hardware_id - Soft binding to hardware (prevents easy sharing)
  • acquired_at - Audit trail for license usage

Technical Implementation

1. Cloud KMS License Signing (Server-Side)

File: backend/licenses/crypto.py (FastAPI)

#!/usr/bin/env python3
"""
Cloud KMS License Signing Service
Generates tamper-proof offline licenses using RSA-4096 asymmetric keys.
"""

import json
import hashlib
import base64
from datetime import datetime, timedelta, timezone
from typing import Dict, Any, Optional

from google.cloud import kms_v1
from google.api_core import exceptions as google_exceptions


class LicenseSigner:
"""
Cloud KMS-based license signing service.

Uses RSA-4096 asymmetric keys for offline license validation.
Signatures are verified client-side without network access.
"""

def __init__(
self,
project_id: str,
location: str = "us-central1",
key_ring: str = "license-keys",
key_name: str = "license-signing-key"
):
"""
Initialize Cloud KMS client.

Args:
project_id: GCP project ID
location: GCP region for KMS
key_ring: KMS key ring name
key_name: Crypto key name
"""
self.client = kms_v1.KeyManagementServiceClient()
self.key_path = self.client.crypto_key_path(
project_id, location, key_ring, key_name
)

def sign_license(
self,
license_key: str,
tier: str,
acquired_at: datetime,
expires_at: datetime,
session_id: str,
hardware_id: str
) -> Dict[str, Any]:
"""
Sign license payload with Cloud KMS.

Args:
license_key: License key (e.g., CODITECT-TEAM-...)
tier: Tier level (free, pro, team, enterprise)
acquired_at: License acquisition timestamp (UTC)
expires_at: License subscription expiration (UTC)
session_id: Unique session identifier
hardware_id: Hardware fingerprint (SHA256)

Returns:
Dictionary with signature and signed payload
"""
# Calculate offline grace period based on tier
offline_grace_hours = {
'free': 24,
'pro': 72,
'team': 48,
'enterprise': 168
}

grace_hours = offline_grace_hours.get(tier.lower(), 24)
offline_expires_at = acquired_at + timedelta(hours=grace_hours)

# Canonicalize payload for signing (sorted keys, no whitespace)
payload = {
'license_key': license_key,
'tier': tier.lower(),
'acquired_at': acquired_at.isoformat(),
'expires_at': expires_at.isoformat(),
'offline_expires_at': offline_expires_at.isoformat(),
'session_id': session_id,
'hardware_id': hardware_id
}

# Create canonical JSON (deterministic serialization)
canonical_json = json.dumps(payload, sort_keys=True, separators=(',', ':'))
message_bytes = canonical_json.encode('utf-8')

# Compute SHA256 digest
digest = hashlib.sha256(message_bytes).digest()

# Create digest object for KMS
digest_obj = kms_v1.Digest()
digest_obj.sha256 = digest

# Sign with Cloud KMS (RSA-4096)
try:
response = self.client.asymmetric_sign(
request={
'name': f"{self.key_path}/cryptoKeyVersions/1",
'digest': digest_obj
}
)

signature = base64.b64encode(response.signature).decode('utf-8')

return {
'payload': payload,
'signature': signature,
'public_key_id': f"{self.key_path}/cryptoKeyVersions/1",
'algorithm': 'RSA_SIGN_PKCS1_4096_SHA256'
}

except google_exceptions.GoogleAPICallError as e:
raise Exception(f"Cloud KMS signing failed: {e}")

def get_public_key(self) -> str:
"""
Retrieve public key for client-side signature verification.

Returns:
PEM-encoded public key string
"""
try:
response = self.client.get_public_key(
request={'name': f"{self.key_path}/cryptoKeyVersions/1"}
)
return response.pem

except google_exceptions.GoogleAPICallError as e:
raise Exception(f"Public key retrieval failed: {e}")


# Example usage
if __name__ == '__main__':
import uuid
from datetime import datetime, timezone, timedelta

# Initialize signer
signer = LicenseSigner(
project_id='coditect-cloud-infra',
location='us-central1'
)

# Sign a license
signed_license = signer.sign_license(
license_key='CODITECT-TEAM-XXXX-YYYY-ZZZZ',
tier='team',
acquired_at=datetime.now(timezone.utc),
expires_at=datetime.now(timezone.utc) + timedelta(days=365),
session_id=str(uuid.uuid4()),
hardware_id=hashlib.sha256(b'hardware_fingerprint').hexdigest()
)

print("Signed License:")
print(json.dumps(signed_license, indent=2))

# Get public key for client
public_key = signer.get_public_key()
print("\nPublic Key (PEM):")
print(public_key[:200] + "..." if len(public_key) > 200 else public_key)

2. Offline License Validation (Client-Side)

File: .coditect/sdk/license_validator.py

#!/usr/bin/env python3
"""
CODITECT License Validator (Client-Side)
Validates licenses offline using Cloud KMS public key verification.
"""

import os
import json
import hashlib
import base64
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Dict, Any, Tuple, Optional

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidSignature

try:
import ntplib
NTP_AVAILABLE = True
except ImportError:
NTP_AVAILABLE = False


class LicenseValidator:
"""
Client-side license validation with offline support.

Features:
- Cloud KMS signature verification (offline-capable)
- Offline grace period enforcement
- Clock skew detection via NTP
- Progressive warnings before expiration
"""

def __init__(self, cache_file: Path = None):
"""
Initialize license validator.

Args:
cache_file: Path to license cache file
"""
if cache_file is None:
cache_file = Path('.coditect/.license-cache')

self.cache_file = cache_file
self.public_key = None

def load_cached_license(self) -> Optional[Dict[str, Any]]:
"""
Load license from cache file.

Returns:
Cached license dictionary or None if not found
"""
if not self.cache_file.exists():
return None

try:
with open(self.cache_file, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
print(f"❌ Error loading cached license: {e}")
return None

def save_cached_license(self, license_data: Dict[str, Any]) -> bool:
"""
Save license to cache file.

Args:
license_data: License dictionary with signature

Returns:
True if successful, False otherwise
"""
try:
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.cache_file, 'w') as f:
json.dump(license_data, f, indent=2)
return True
except IOError as e:
print(f"❌ Error saving license cache: {e}")
return False

def verify_signature(
self,
license_data: Dict[str, Any],
public_key_pem: str
) -> bool:
"""
Verify license signature using Cloud KMS public key.

Args:
license_data: License dictionary with payload and signature
public_key_pem: PEM-encoded public key

Returns:
True if signature valid, False otherwise
"""
try:
# Extract payload and signature
payload = license_data.get('payload')
signature_b64 = license_data.get('signature')

if not payload or not signature_b64:
print("❌ License missing payload or signature")
return False

# Canonicalize payload
canonical_json = json.dumps(
payload,
sort_keys=True,
separators=(',', ':')
)
message_bytes = canonical_json.encode('utf-8')

# Decode signature
signature = base64.b64decode(signature_b64)

# Load public key
public_key = serialization.load_pem_public_key(
public_key_pem.encode('utf-8'),
backend=default_backend()
)

# Verify signature (RSA-4096 with SHA256)
public_key.verify(
signature,
message_bytes,
padding.PKCS1v15(),
hashes.SHA256()
)

return True

except InvalidSignature:
print("❌ License signature verification failed (tampered)")
return False
except Exception as e:
print(f"❌ Signature verification error: {e}")
return False

def detect_clock_skew(self) -> Tuple[bool, Optional[str]]:
"""
Detect system clock skew using NTP.

Returns:
Tuple of (skew_detected, error_message)
"""
if not NTP_AVAILABLE:
return False, None

try:
ntp_client = ntplib.NTPClient()
response = ntp_client.request('pool.ntp.org', timeout=2)

# Get NTP time
ntp_time = datetime.fromtimestamp(response.tx_time, timezone.utc)

# Get local time
local_time = datetime.now(timezone.utc)

# Calculate skew
skew_seconds = abs((ntp_time - local_time).total_seconds())

# Flag if skew >5 minutes (300 seconds)
if skew_seconds > 300:
return True, f"Clock skew detected: {int(skew_seconds)}s"

return False, None

except Exception as e:
# NTP check failed (offline or blocked)
# Don't fail validation, just log warning
return False, None

def validate_offline_license(
self,
license_data: Dict[str, Any],
public_key_pem: str
) -> Tuple[bool, str]:
"""
Validate license for offline usage.

Args:
license_data: Cached license with signature
public_key_pem: Cloud KMS public key (PEM format)

Returns:
Tuple of (valid, message)
"""
# Step 1: Verify signature (tamper detection)
if not self.verify_signature(license_data, public_key_pem):
return False, "License signature invalid (tampered)"

payload = license_data['payload']

# Step 2: Check license subscription expiration
expires_at = datetime.fromisoformat(payload['expires_at'])
now = datetime.now(timezone.utc)

if now > expires_at:
return False, "License subscription expired"

# Step 3: Check offline grace period
offline_expires_at = datetime.fromisoformat(payload['offline_expires_at'])

if now > offline_expires_at:
return False, "Offline grace period expired - internet connection required"

# Step 4: Calculate time remaining
time_remaining = offline_expires_at - now
hours_remaining = int(time_remaining.total_seconds() / 3600)

# Step 5: Display warnings based on time remaining
if hours_remaining < 1:
print("🚨 CRITICAL: Offline license expires in <1 hour!")
print(" IMMEDIATE ACTION REQUIRED: Connect to internet NOW.")
elif hours_remaining < 6:
print(f"⚠️ URGENT: Offline license expires in {hours_remaining} hours!")
print(" CODITECT will stop working without internet connection.")
print(" Save your work and connect to renew your license.")
elif hours_remaining < 12:
print(f"⚠️ IMPORTANT: Offline license expires in {hours_remaining} hours.")
print(" Please connect to the internet to avoid losing access.")
elif hours_remaining < 24:
print(f"⚠️ Offline license expires in {hours_remaining} hours. Connect to internet soon to renew.")

# Step 6: Check for clock skew (optional)
skew_detected, skew_msg = self.detect_clock_skew()
if skew_detected:
print(f"⚠️ {skew_msg}")
print(" Your system clock may be incorrect. This could affect license validation.")

return True, f"Valid (offline mode, {hours_remaining}h remaining)"

def calculate_grace_period(self, tier: str) -> int:
"""
Calculate offline grace period in hours for tier.

Args:
tier: Tier name (free, pro, team, enterprise)

Returns:
Grace period in hours
"""
grace_periods = {
'free': 24,
'pro': 72,
'team': 48,
'enterprise': 168
}
return grace_periods.get(tier.lower(), 24)


# Example usage
if __name__ == '__main__':
import sys

# Initialize validator
validator = LicenseValidator()

# Load cached license
cached_license = validator.load_cached_license()

if not cached_license:
print("❌ No cached license found")
sys.exit(1)

# Public key (normally fetched from license server or embedded)
public_key_pem = """-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1234567890...
-----END PUBLIC KEY-----"""

# Validate offline license
valid, message = validator.validate_offline_license(
cached_license,
public_key_pem
)

if valid:
print(f"✅ {message}")
sys.exit(0)
else:
print(f"❌ {message}")
sys.exit(1)

3. Integration with init.sh

File: .coditect/scripts/init.sh (Enhanced Offline Validation)

#!/bin/bash
# CODITECT Initialization Script with Offline Grace Period Support

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CACHE_FILE=".coditect/.license-cache"
LICENSE_VALIDATOR="$SCRIPT_DIR/../sdk/license_validator.py"

# ============================================
# SECTION 1: License Validation
# ============================================

echo "🔒 Validating CODITECT license..."

# Step 1: Check for cached license
if [ -f "$CACHE_FILE" ]; then
echo "📁 Found cached license, validating offline..."

# Validate cached license (offline mode)
python3 "$LICENSE_VALIDATOR" validate-offline
VALIDATION_STATUS=$?

if [ $VALIDATION_STATUS -eq 0 ]; then
echo "✅ License valid (offline mode)"

# Start heartbeat in background (will fail silently if offline)
python3 "$SCRIPT_DIR/../sdk/license_client.py" heartbeat --daemon &
HEARTBEAT_PID=$!
echo $HEARTBEAT_PID > .coditect/.heartbeat.pid

# Continue to framework initialization
echo ""
echo "📦 CODITECT Framework v1.0"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# ... rest of init.sh (environment checks, component activation, etc.)
exit 0
else
echo "⚠️ Cached license invalid or expired, attempting online validation..."
fi
fi

# Step 2: Online validation (no cached license or cache invalid)
echo "⚡ Contacting license server..."

# Check internet connectivity first
if ! ping -c 1 license.coditect.ai &> /dev/null; then
echo "❌ License server unreachable"
echo ""
echo "Your CODITECT license could not be validated."
echo ""
echo "Reasons:"
echo " • No internet connection"
echo " • Cached license expired or invalid"
echo ""
echo "Solutions:"
echo " 1. Connect to the internet"
echo " 2. Activate your license: coditect activate <license-key>"
echo " 3. Contact support: support@coditect.ai"
echo ""
exit 1
fi

# Acquire license from API
python3 "$SCRIPT_DIR/../sdk/license_client.py" acquire
if [ $? -eq 0 ]; then
echo "✅ License activated"

# Start heartbeat in background
python3 "$SCRIPT_DIR/../sdk/license_client.py" heartbeat --daemon &
HEARTBEAT_PID=$!
echo $HEARTBEAT_PID > .coditect/.heartbeat.pid

# Continue to framework initialization
echo ""
echo "📦 CODITECT Framework v1.0"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# ... rest of init.sh
exit 0
else
echo "❌ License activation failed"
echo ""
echo "To activate your license:"
echo " 1. Visit: https://coditect.ai/activate"
echo " 2. Sign in with your account"
echo " 3. Copy your license key"
echo " 4. Run: coditect activate <license-key>"
echo ""
echo "Need help? Contact: support@coditect.ai"
echo ""
exit 1
fi

4. Clock Skew Detection

File: .coditect/sdk/clock_skew_detector.py

#!/usr/bin/env python3
"""
Clock Skew Detection via NTP
Detects system clock manipulation to prevent grace period abuse.
"""

import time
from datetime import datetime, timezone
from typing import Tuple, Optional

try:
import ntplib
NTP_AVAILABLE = True
except ImportError:
NTP_AVAILABLE = False


class ClockSkewDetector:
"""
Detects system clock skew using multiple NTP servers.

Features:
- Multiple NTP server fallback
- Configurable skew threshold
- Retry logic with exponential backoff
"""

# Public NTP server pool
NTP_SERVERS = [
'pool.ntp.org',
'time.google.com',
'time.cloudflare.com',
'time.apple.com'
]

def __init__(self, max_skew_seconds: int = 300):
"""
Initialize clock skew detector.

Args:
max_skew_seconds: Maximum acceptable clock skew (default 5 minutes)
"""
self.max_skew_seconds = max_skew_seconds

def detect_skew(self, retry_count: int = 3) -> Tuple[bool, Optional[str], Optional[float]]:
"""
Detect clock skew using NTP.

Args:
retry_count: Number of retry attempts

Returns:
Tuple of (skew_detected, error_message, skew_seconds)
"""
if not NTP_AVAILABLE:
return False, "NTP library not available", None

for server in self.NTP_SERVERS:
for attempt in range(retry_count):
try:
# Query NTP server
ntp_client = ntplib.NTPClient()
response = ntp_client.request(server, timeout=2)

# Get timestamps
ntp_time = datetime.fromtimestamp(response.tx_time, timezone.utc)
local_time = datetime.now(timezone.utc)

# Calculate skew
skew_seconds = abs((ntp_time - local_time).total_seconds())

# Check if skew exceeds threshold
if skew_seconds > self.max_skew_seconds:
return True, f"Clock skew detected: {int(skew_seconds)}s (threshold: {self.max_skew_seconds}s)", skew_seconds

# Skew within acceptable range
return False, None, skew_seconds

except ntplib.NTPException as e:
# NTP query failed, try next server
if attempt == retry_count - 1:
# Last retry for this server
continue
time.sleep(0.5 * (2 ** attempt)) # Exponential backoff

except Exception as e:
# Other errors, try next server
continue

# All servers failed (likely offline)
return False, "NTP query failed (offline or blocked)", None

def get_ntp_timestamp(self) -> Optional[datetime]:
"""
Get current NTP timestamp.

Returns:
NTP timestamp or None if unavailable
"""
if not NTP_AVAILABLE:
return None

for server in self.NTP_SERVERS:
try:
ntp_client = ntplib.NTPClient()
response = ntp_client.request(server, timeout=2)
return datetime.fromtimestamp(response.tx_time, timezone.utc)
except:
continue

return None


# Example usage
if __name__ == '__main__':
detector = ClockSkewDetector(max_skew_seconds=300)

skew_detected, message, skew_seconds = detector.detect_skew()

if skew_detected:
print(f"⚠️ {message}")
print(f" Your system clock is {int(skew_seconds / 3600)} hours off.")
print(f" This may affect license validation.")
elif skew_seconds is not None:
print(f"✅ Clock skew within acceptable range ({int(skew_seconds)}s)")
else:
print(f"⚠️ {message}")

5. Graceful Shutdown Handler

File: .coditect/scripts/shutdown.sh

#!/bin/bash
# Release license seat on CODITECT shutdown

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

echo "🛑 Shutting down CODITECT..."

# Stop heartbeat process
if [ -f .coditect/.heartbeat.pid ]; then
HEARTBEAT_PID=$(cat .coditect/.heartbeat.pid)
if kill -0 "$HEARTBEAT_PID" 2>/dev/null; then
kill "$HEARTBEAT_PID" 2>/dev/null || true
echo "✅ Stopped heartbeat process"
fi
rm -f .coditect/.heartbeat.pid
fi

# Release seat (best effort - may fail if offline)
python3 "$SCRIPT_DIR/../sdk/license_client.py" release 2>/dev/null || true

# Keep cached license for offline usage
# DO NOT delete .coditect/.license-cache

echo "✅ Shutdown complete"
exit 0

Security Considerations

Threat Model

ThreatImpactMitigationResidual Risk
Clock manipulationExtend grace period indefinitelyNTP clock skew detectionDetermined attacker with root access can bypass
License sharingRevenue leakageHardware ID soft binding + session IDsUsers can share hardware fingerprints
License tamperingForge offline licensesCloud KMS RSA-4096 signatureNone (cryptographically secure)
Cache reuseUse expired licensesSession ID uniqueness checkNone
Public key substitutionReplace public key to accept forged signaturesEmbed public key in binary or fetch via HTTPSRequires recompiling CODITECT

Security Best Practices

1. Signature Algorithm: RSA-4096 with SHA256

# Why RSA-4096?
# - 128-bit security level (post-quantum secure until ~2030)
# - Industry standard (FIPS 140-2 compliant)
# - Compatible with Cloud KMS asymmetric signing
# - Signature size: 512 bytes (acceptable overhead)

# Why not Ed25519?
# - Not supported by Cloud KMS asymmetric signing API
# - Smaller signatures (64 bytes) but requires Edwards curve support

# Why not ECDSA?
# - Nonce reuse vulnerability risk
# - Deterministic ECDSA (RFC 6979) not widely supported

2. Public Key Distribution

Option A: Embed in CODITECT binary (Recommended for v1.0)

# .coditect/sdk/public_key.py
PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA...
-----END PUBLIC KEY-----"""

# Pros: Works offline immediately
# Cons: Requires CODITECT rebuild to rotate keys

Option B: Fetch via HTTPS on first run (Recommended for v2.0)

# Download public key from license server
response = requests.get(
'https://license.coditect.ai/v1/public-key',
timeout=10
)
public_key_pem = response.text

# Cache locally for offline usage
with open('.coditect/.public-key.pem', 'w') as f:
f.write(public_key_pem)

# Pros: Key rotation without CODITECT rebuild
# Cons: Requires internet on first run

3. Canonicalization

# CRITICAL: Use deterministic JSON serialization for signing
# Different JSON serialization = different signature

# ❌ WRONG - Non-deterministic (whitespace, key order)
message = json.dumps({'tier': 'pro', 'key': 'ABC'})
# Output: {"tier": "pro", "key": "ABC"}

message = json.dumps({'key': 'ABC', 'tier': 'pro'})
# Output: {"key": "ABC", "tier": "pro"} # DIFFERENT!

# ✅ CORRECT - Canonical form (sorted keys, no whitespace)
message = json.dumps(payload, sort_keys=True, separators=(',', ':'))
# Output: {"key":"ABC","tier":"pro"} # ALWAYS SAME

4. Session ID Uniqueness

# Prevent license cache reuse across sessions
import uuid
import hashlib

def generate_session_id(user_email: str, hardware_id: str, timestamp: str) -> str:
"""
Generate unique session ID for license.

Combines:
- User email (identity)
- Hardware ID (device binding)
- Timestamp (time uniqueness)
- Random UUID (entropy)
"""
session_data = f"{user_email}:{hardware_id}:{timestamp}:{uuid.uuid4()}"
session_id = hashlib.sha256(session_data.encode()).hexdigest()
return session_id

# Each license acquisition gets new session_id
# Prevents reusing old cached licenses across different sessions

Audit and Compliance

SOC 2 Type II Requirements:

  • ✅ Encryption at rest (Cloud KMS CMEK)
  • ✅ Encryption in transit (TLS 1.3)
  • ✅ Audit logging (all license operations logged)
  • ✅ Access controls (IAM service accounts)
  • ✅ Key rotation (automated annual rotation)

GDPR Requirements:

  • ✅ User email stored only with consent
  • ✅ License data deletable (right to erasure)
  • ✅ Minimal hardware fingerprinting (SHA256 hash, not raw data)
  • ✅ Data retention policy (90 days for inactive licenses)

Testing Strategy

Unit Tests

File: tests/unit/test_license_validator.py

#!/usr/bin/env python3
"""
Unit tests for offline license validation.
Coverage target: 95%+
"""

import pytest
import json
import hashlib
from datetime import datetime, timedelta, timezone
from pathlib import Path

from sdk.license_validator import LicenseValidator
from sdk.clock_skew_detector import ClockSkewDetector


class TestLicenseValidator:
"""Test suite for offline license validation."""

@pytest.fixture
def validator(self, tmp_path):
"""Create validator with temporary cache file."""
cache_file = tmp_path / ".license-cache"
return LicenseValidator(cache_file=cache_file)

@pytest.fixture
def valid_license(self):
"""Generate valid license for testing."""
now = datetime.now(timezone.utc)

return {
'payload': {
'license_key': 'CODITECT-TEAM-TEST-1234-5678',
'tier': 'team',
'acquired_at': now.isoformat(),
'expires_at': (now + timedelta(days=365)).isoformat(),
'offline_expires_at': (now + timedelta(hours=48)).isoformat(),
'session_id': hashlib.sha256(b'test_session').hexdigest(),
'hardware_id': hashlib.sha256(b'test_hardware').hexdigest()
},
'signature': 'base64_encoded_signature_placeholder',
'public_key_id': 'projects/.../cryptoKeyVersions/1'
}

def test_save_and_load_license(self, validator, valid_license):
"""Test license caching."""
# Save license
assert validator.save_cached_license(valid_license)

# Load license
loaded = validator.load_cached_license()
assert loaded == valid_license

def test_expired_offline_grace(self, validator, valid_license):
"""Test expired offline grace period."""
# Set offline_expires_at to past
past = datetime.now(timezone.utc) - timedelta(hours=1)
valid_license['payload']['offline_expires_at'] = past.isoformat()

# Should fail validation
valid, message = validator.validate_offline_license(
valid_license,
"fake_public_key" # Signature check mocked
)

assert not valid
assert "Offline grace period expired" in message

def test_grace_period_warnings(self, validator, valid_license, capsys):
"""Test progressive warnings before expiration."""
# Test 23 hours remaining (should warn)
near_expiry = datetime.now(timezone.utc) + timedelta(hours=23)
valid_license['payload']['offline_expires_at'] = near_expiry.isoformat()

validator.validate_offline_license(valid_license, "fake_public_key")

captured = capsys.readouterr()
assert "⚠️" in captured.out
assert "23 hours" in captured.out

def test_tier_grace_calculation(self, validator):
"""Test grace period calculation for each tier."""
assert validator.calculate_grace_period('free') == 24
assert validator.calculate_grace_period('pro') == 72
assert validator.calculate_grace_period('team') == 48
assert validator.calculate_grace_period('enterprise') == 168

# Unknown tier defaults to 24h
assert validator.calculate_grace_period('unknown') == 24


class TestClockSkewDetector:
"""Test suite for clock skew detection."""

def test_normal_clock_skew(self):
"""Test clock within acceptable range."""
detector = ClockSkewDetector(max_skew_seconds=300)

skew_detected, message, skew_seconds = detector.detect_skew()

# Should not detect skew if clock is accurate
assert not skew_detected or skew_seconds is None

def test_offline_ntp_failure(self, monkeypatch):
"""Test NTP failure (offline scenario)."""
import ntplib

# Mock NTP client to always fail
def mock_request(*args, **kwargs):
raise ntplib.NTPException("Network unreachable")

monkeypatch.setattr(ntplib.NTPClient, 'request', mock_request)

detector = ClockSkewDetector()
skew_detected, message, skew_seconds = detector.detect_skew()

# Should not fail validation, just log warning
assert not skew_detected
assert skew_seconds is None
assert "offline" in message.lower()

Integration Tests

File: tests/integration/test_offline_workflow.py

#!/usr/bin/env python3
"""
Integration tests for offline license workflow.
Tests complete flow: online acquisition → offline validation → expiration.
"""

import pytest
import time
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path


class TestOfflineWorkflow:
"""End-to-end offline license workflow tests."""

@pytest.fixture
def clean_cache(self):
"""Remove license cache before each test."""
cache_file = Path('.coditect/.license-cache')
if cache_file.exists():
cache_file.unlink()
yield
if cache_file.exists():
cache_file.unlink()

def test_online_acquisition_then_offline(self, clean_cache):
"""
Test normal workflow:
1. Acquire license online
2. Validate offline successfully
"""
# Step 1: Acquire license (requires license server)
result = subprocess.run(
['python3', '.coditect/sdk/license_client.py', 'acquire'],
capture_output=True,
text=True
)

assert result.returncode == 0, "License acquisition failed"

# Step 2: Validate offline (no network required)
result = subprocess.run(
['python3', '.coditect/sdk/license_validator.py', 'validate-offline'],
capture_output=True,
text=True
)

assert result.returncode == 0, "Offline validation failed"
assert "✅" in result.stdout

def test_grace_period_expiration_warning(self, clean_cache):
"""
Test warning system:
1. Acquire license
2. Mock time near expiration
3. Verify warnings displayed
"""
# Acquire license
subprocess.run(
['python3', '.coditect/sdk/license_client.py', 'acquire'],
check=True
)

# TODO: Mock time to near-expiration
# Requires time mocking library (freezegun or similar)

# Validate and check for warnings
result = subprocess.run(
['python3', '.coditect/sdk/license_validator.py', 'validate-offline'],
capture_output=True,
text=True
)

# Should show warning
assert "⚠️" in result.stdout or "✅" in result.stdout

def test_expired_grace_period(self, clean_cache):
"""
Test expired grace period:
1. Acquire license
2. Mock time past expiration
3. Verify validation fails
"""
# Acquire license
subprocess.run(
['python3', '.coditect/sdk/license_client.py', 'acquire'],
check=True
)

# TODO: Mock time to past expiration

# Validation should fail
result = subprocess.run(
['python3', '.coditect/sdk/license_validator.py', 'validate-offline'],
capture_output=True,
text=True
)

# Should fail with expired message
# (Currently passes because time not mocked)

Load Tests

File: tests/load/test_offline_performance.py

#!/usr/bin/env python3
"""
Load tests for offline license validation performance.
Target: <10ms p95 latency for signature verification.
"""

import time
import statistics
from sdk.license_validator import LicenseValidator


def test_signature_verification_performance():
"""
Benchmark signature verification latency.

Target performance:
- p50: <5ms
- p95: <10ms
- p99: <20ms
"""
validator = LicenseValidator()

# Mock license data
license_data = {
'payload': {
'license_key': 'TEST',
'tier': 'pro',
# ... other fields
},
'signature': 'base64_signature'
}

public_key_pem = """-----BEGIN PUBLIC KEY-----
... (test public key)
-----END PUBLIC KEY-----"""

# Run 1000 verifications
latencies = []

for _ in range(1000):
start = time.perf_counter()
validator.verify_signature(license_data, public_key_pem)
latency_ms = (time.perf_counter() - start) * 1000
latencies.append(latency_ms)

# Calculate percentiles
p50 = statistics.median(latencies)
p95 = statistics.quantiles(latencies, n=20)[18] # 95th percentile
p99 = statistics.quantiles(latencies, n=100)[98] # 99th percentile

print(f"Signature Verification Performance:")
print(f" p50: {p50:.2f}ms")
print(f" p95: {p95:.2f}ms")
print(f" p99: {p99:.2f}ms")

# Assert performance targets
assert p50 < 5.0, f"p50 latency {p50:.2f}ms exceeds 5ms target"
assert p95 < 10.0, f"p95 latency {p95:.2f}ms exceeds 10ms target"
assert p99 < 20.0, f"p99 latency {p99:.2f}ms exceeds 20ms target"


if __name__ == '__main__':
test_signature_verification_performance()

Monitoring and Observability

Metrics to Track

Prometheus Metrics:

# backend/licenses/metrics.py

from prometheus_client import Counter, Histogram, Gauge

# License acquisition metrics
license_acquired_total = Counter(
'license_acquired_total',
'Total licenses acquired',
['tier']
)

license_acquisition_latency = Histogram(
'license_acquisition_latency_seconds',
'License acquisition latency',
['tier'],
buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0]
)

# Offline validation metrics
offline_validation_total = Counter(
'offline_validation_total',
'Total offline validations',
['tier', 'result'] # result: success, expired, tampered
)

offline_grace_remaining_hours = Histogram(
'offline_grace_remaining_hours',
'Offline grace period remaining (hours)',
['tier'],
buckets=[1, 6, 12, 24, 48, 72, 168]
)

# Grace period expiration warnings
grace_expiration_warnings = Counter(
'grace_expiration_warnings_total',
'Grace period expiration warnings shown',
['tier', 'threshold'] # threshold: 24h, 12h, 6h, 1h
)

# Clock skew detection
clock_skew_detected_total = Counter(
'clock_skew_detected_total',
'Clock skew detections',
['tier']
)

clock_skew_seconds = Histogram(
'clock_skew_seconds',
'Detected clock skew magnitude',
buckets=[0, 60, 300, 900, 3600, 86400] # 0s, 1min, 5min, 15min, 1hr, 1day
)

# Signature verification metrics
signature_verification_latency = Histogram(
'signature_verification_latency_seconds',
'Signature verification latency',
buckets=[0.001, 0.005, 0.010, 0.050, 0.100] # 1ms, 5ms, 10ms, 50ms, 100ms
)

Grafana Dashboard

Dashboard Panels:

  1. Offline License Usage

    • Active offline licenses by tier (gauge)
    • Offline validation success rate (%)
    • Grace period distribution (histogram)
  2. Grace Period Health

    • Licenses expiring within 24h (count)
    • Average grace period remaining (hours)
    • Warning events timeline
  3. Security Monitoring

    • Signature verification failures (count)
    • Clock skew detections (count)
    • Tampered license attempts (count)
  4. Performance Metrics

    • Signature verification latency (p50, p95, p99)
    • License acquisition latency
    • Offline validation latency

Alerts

Prometheus Alerting Rules:

# alerts/offline_licenses.yml

groups:
- name: offline_licenses
rules:
- alert: HighOfflineGraceExpirations
expr: rate(grace_expiration_warnings_total[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "High rate of grace period expirations"
description: "{{ $value }} grace period warnings in last 5 minutes"

- alert: ClockSkewDetected
expr: rate(clock_skew_detected_total[5m]) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "Clock skew detected on multiple systems"
description: "{{ $value }} clock skew events in last 5 minutes"

- alert: SignatureVerificationFailures
expr: rate(offline_validation_total{result="tampered"}[5m]) > 1
for: 5m
labels:
severity: critical
annotations:
summary: "License tampering attempts detected"
description: "{{ $value }} tampered licenses detected in last 5 minutes"

Upstream Dependencies

  • ADR-001: Floating Licenses vs. Node-Locked Licenses

  • ADR-003: Check-on-Init Enforcement Pattern

  • ADR-006: Cloud KMS License Signing (Future)

    • Defines RSA-4096 asymmetric key usage
    • Specifies signature algorithm (RSASSA-PKCS1-v1_5 with SHA256)
    • Establishes public key distribution mechanism
    • Describes key rotation strategy
    • Link: ADR-006 (to be created)

Downstream Impacts

  • ADR-004: Symlink Resolution Strategy

    • Session ID generation includes resolved symlink paths
    • Offline licenses tied to specific session_id
    • Link: ADR-004
  • ADR-005: Builder vs. Runtime Licensing Model

    • Offline grace periods apply to both builder and runtime modes
    • Link: ADR-005

Cross-Cutting Concerns

  • Security: Cloud KMS key management, signature verification
  • User Experience: Progressive warnings, transparent online/offline transitions
  • Monitoring: Offline validation metrics, grace period health
  • Compliance: GDPR (minimal hardware fingerprinting), SOC 2 (audit logging)

References

External Standards

Industry Best Practices

GCP Documentation

Python Libraries


Last Updated: 2025-11-30 Owner: Architecture Team, Security Team Review Cycle: Quarterly or on security/licensing infrastructure changes Implementation Status: Ready for Development (Phase 2)