ADR-006: Cloud KMS License Signing
Status: Accepted Date: 2025-11-30 Decision Makers: Security Team, Cloud Architecture Team Related ADRs: ADR-001 (Floating Licenses), ADR-003 (Check-on-Init), ADR-008 (Offline Grace Period), ADR-009 (GCP Infrastructure)
Context
Problem Statement
CODITECT licenses must be signed with tamper-proof cryptographic signatures to enable:
- Offline License Validation - Clients verify licenses locally without internet
- Tamper Detection - Prevent license modification or key forgery
- Grace Period Enforcement - Securely validate offline_expires_at timestamps
- Key Rotation - Rotate signing keys without breaking existing licenses
Security Requirements:
- RSA-4096 asymmetric signing (industry standard for high-security applications)
- Hardware-backed keys (HSM/TPM) to prevent private key extraction
- Automatic key rotation (90-day cycle)
- Audit logging for all signing operations
- FIPS 140-2 compliance for cryptographic operations
From ADR-008: Offline grace periods (24-168 hours depending on tier) require signed licenses that clients can verify without network access.
Current Architecture
License Activation Flow:
1. Client requests license → Backend validates user/license
2. Backend signs license payload with Cloud KMS
3. Client receives signed license + public key
4. Client verifies signature locally (works offline)
5. Client checks offline_expires_at timestamp
Business Context
Cost of License Tampering:
- Unlimited license usage without payment
- Revenue loss: $50/user/month × 1,000 pirated users = $50K/month
- Reputational damage from easy piracy
Compliance Requirements:
- SOC 2 Type II: Cryptographic key management
- FIPS 140-2: Government/enterprise customers require FIPS-compliant crypto
Decision
Use Google Cloud KMS with RSA-4096 asymmetric keys for license signing.
Architecture
Key Components
1. Cloud KMS Key Ring
# terraform/modules/kms/main.tf
resource "google_kms_key_ring" "license_signing" {
name = "license-signing"
location = "us-central1"
}
resource "google_kms_crypto_key" "rsa_4096" {
name = "rsa-4096"
key_ring = google_kms_key_ring.license_signing.id
purpose = "ASYMMETRIC_SIGN"
version_template {
algorithm = "RSA_SIGN_PSS_4096_SHA256"
protection_level = "HSM" # Hardware Security Module
}
rotation_period = "7776000s" # 90 days
lifecycle {
prevent_destroy = true # Never delete production keys
}
}
# IAM: Only license-server can sign
resource "google_kms_crypto_key_iam_member" "signer" {
crypto_key_id = google_kms_crypto_key.rsa_4096.id
role = "roles/cloudkms.signerVerifier"
member = "serviceAccount:license-server@project.iam.gserviceaccount.com"
}
2. License Signing Service (Python)
# backend/licenses/kms.py
from google.cloud import kms
import hashlib
import json
import base64
from datetime import datetime, timedelta
class LicenseSigner:
"""Cloud KMS license signing service."""
def __init__(self, project_id: str, location: str, key_ring: str, key_name: str):
self.client = kms.KeyManagementServiceClient()
self.key_name = (
f"projects/{project_id}/locations/{location}/"
f"keyRings/{key_ring}/cryptoKeys/{key_name}/cryptoKeyVersions/1"
)
def sign_license(self, license_data: dict) -> dict:
"""
Sign license payload with Cloud KMS RSA-4096.
Args:
license_data: License payload (license_key, tier, features, etc.)
Returns:
Signed license with signature and public_key_id
"""
# Canonical JSON serialization (deterministic ordering)
payload_json = json.dumps(license_data, sort_keys=True, separators=(',', ':'))
# SHA-256 digest
digest = hashlib.sha256(payload_json.encode()).digest()
# Cloud KMS signing (HSM-backed)
response = self.client.asymmetric_sign(
request={
"name": self.key_name,
"digest": {"sha256": digest}
}
)
# Base64 encode signature
signature = base64.b64encode(response.signature).decode()
return {
"payload": license_data,
"signature": signature,
"public_key_id": self.key_name,
"algorithm": "RSA_SIGN_PSS_4096_SHA256",
"signed_at": datetime.utcnow().isoformat() + "Z"
}
def get_public_key(self) -> str:
"""Get public key for client-side verification."""
response = self.client.get_public_key(request={"name": self.key_name})
return response.pem
# Usage in FastAPI endpoint
signer = LicenseSigner(
project_id="coditect-cloud-infra",
location="us-central1",
key_ring="license-signing",
key_name="rsa-4096"
)
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import status
from django.utils import timezone
from datetime import timedelta
@api_view(['POST'])
@permission_classes([AllowAny])
def activate_license(request):
# ... validate license ...
# Calculate offline expiration
tier_grace_hours = {"free": 24, "pro": 72, "team": 48, "enterprise": 168}
offline_expires_at = timezone.now() + timedelta(
hours=tier_grace_hours[license.tier]
)
# Prepare license payload
license_data = {
"license_key": license.license_key,
"tier": license.tier,
"features": license.features,
"expires_at": license.expires_at.isoformat() + "Z",
"offline_expires_at": offline_expires_at.isoformat() + "Z",
"session_id": request.data.get('session_id'),
"hardware_id": request.data.get('hardware_id'),
"issued_at": timezone.now().isoformat() + "Z"
}
# Sign with Cloud KMS
signed_license = signer.sign_license(license_data)
return Response(signed_license, status=status.HTTP_200_OK)
3. Client-Side Verification (Python)
# .coditect/sdk/license_verifier.py
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
import base64
import json
from datetime import datetime
class LicenseVerifier:
"""Client-side license signature verification (offline-capable)."""
def __init__(self, public_key_pem: str):
"""
Initialize verifier with public key.
Args:
public_key_pem: PEM-encoded RSA public key from Cloud KMS
"""
self.public_key = serialization.load_pem_public_key(
public_key_pem.encode()
)
def verify_license(self, signed_license: dict) -> tuple[bool, str]:
"""
Verify license signature (works offline).
Args:
signed_license: {payload, signature, public_key_id, algorithm}
Returns:
(valid: bool, message: str)
"""
try:
# Canonical JSON serialization (must match signing)
payload_json = json.dumps(
signed_license['payload'],
sort_keys=True,
separators=(',', ':')
)
# Decode base64 signature
signature = base64.b64decode(signed_license['signature'])
# Verify RSA-PSS signature
self.public_key.verify(
signature,
payload_json.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# Signature valid - check expiration
offline_expires_at = datetime.fromisoformat(
signed_license['payload']['offline_expires_at'].replace('Z', '+00:00')
)
if datetime.utcnow() < offline_expires_at:
return True, "License signature valid and not expired"
else:
return False, "License signature valid but offline grace period expired"
except Exception as e:
return False, f"License signature invalid: {e}"
# Usage in init.sh
def validate_cached_license():
"""Validate license from cache (offline mode)."""
# Load cached license
with open('.coditect/.license-cache', 'r') as f:
cached_license = json.load(f)
# Get public key (embedded in SDK or cached)
public_key_pem = get_public_key() # From SDK resources
# Verify signature
verifier = LicenseVerifier(public_key_pem)
valid, message = verifier.verify_license(cached_license)
if valid:
print(f"✅ License valid (offline mode)")
return True
else:
print(f"❌ License invalid: {message}")
return False
License Signature Structure
{
"payload": {
"license_key": "CODITECT-PRO-2024-XXXX",
"tier": "pro",
"features": {
"max_agents": 52,
"max_commands": 81,
"max_projects": -1,
"offline_grace_hours": 72
},
"expires_at": "2026-11-30T00:00:00Z",
"offline_expires_at": "2025-12-02T12:00:00Z",
"session_id": "abc123...",
"hardware_id": "def456...",
"issued_at": "2025-11-30T12:00:00Z"
},
"signature": "base64_rsa_4096_signature_here...",
"public_key_id": "projects/coditect-cloud-infra/locations/us-central1/keyRings/license-signing/cryptoKeys/rsa-4096/cryptoKeyVersions/1",
"algorithm": "RSA_SIGN_PSS_4096_SHA256",
"signed_at": "2025-11-30T12:00:00Z"
}
Consequences
Positive
✅ Tamper-Proof Licenses
- RSA-4096 signature prevents license modification
- Changing any field (tier, expires_at, offline_expires_at) invalidates signature
- Attacker cannot forge licenses without private key
✅ Offline Verification
- Public key verification works without internet
- Client validates signature locally in <10ms
- Enables offline grace periods (24-168 hours)
✅ HSM-Backed Security
- Private key stored in Cloud KMS HSM (FIPS 140-2 Level 3)
- Private key cannot be exported or extracted
- Key compromise requires physical HSM access
✅ Automatic Key Rotation
- Cloud KMS rotates keys every 90 days automatically
- Old keys remain valid for existing licenses
- No manual intervention required
✅ Audit Logging
- Cloud KMS logs all signing operations to Cloud Audit Logs
- Track: who signed, when, what license_key
- Detect anomalous signing patterns (e.g., 1000 signatures in 1 minute)
✅ FIPS 140-2 Compliance
- Cloud KMS HSM is FIPS 140-2 Level 3 certified
- Meets government/enterprise security requirements
- Enables sales to regulated industries (healthcare, finance, government)
Negative
⚠️ Cloud Dependency for Signing
- License signing requires GCP connectivity
- Cannot sign licenses offline (only verify)
- Mitigation: Pre-sign licenses for offline distribution
⚠️ Cost
- Key storage: $1/month per active key version
- Signing operations: $0.03 per 10,000 operations
- Example: 10,000 license activations/month = $1.03/month total
- Trade-off: Acceptable cost for tamper-proof security
⚠️ Latency
- Cloud KMS signing adds 50-100ms per request
- Mitigation: Sign once, cache signed license
- Mitigation: Async signing for non-critical paths
⚠️ Key Export Impossible
- Cannot export private key from Cloud KMS
- Locked into GCP forever (cannot migrate to AWS/Azure)
- Mitigation: Accept vendor lock-in for security benefits
⚠️ Public Key Distribution
- Client must have correct public key
- Risk: Attacker replaces public key with their own
- Mitigation: Embed public key in signed SDK binary
Alternatives Considered
Alternative 1: HMAC Symmetric Signing
Architecture: Shared secret key for HMAC-SHA256 signatures.
import hmac
import hashlib
# Backend (signing)
secret_key = "shared-secret-key"
signature = hmac.new(secret_key.encode(), payload.encode(), hashlib.sha256).hexdigest()
# Client (verification)
expected_signature = hmac.new(secret_key.encode(), payload.encode(), hashlib.sha256).hexdigest()
valid = signature == expected_signature
Pros: ✅ Faster (no asymmetric crypto, <1ms) ✅ Simpler implementation ✅ No Cloud KMS dependency
Cons: ❌ Shared secret on client - Client has signing key (can forge licenses) ❌ Cannot verify offline securely - Secret must be embedded in client ❌ Key rotation requires client updates - Cannot rotate without breaking all clients
Decision: REJECTED - Shared secret enables unlimited license forgery.
Alternative 2: Application-Level RSA Keys
Architecture: Generate RSA-4096 key pair, store private key in application code.
from cryptography.hazmat.primitives.asymmetric import rsa
# Generate key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
public_key = private_key.public_key()
# Store in environment variable
PRIVATE_KEY_PEM = os.environ['PRIVATE_KEY_PEM']
Pros: ✅ No Cloud KMS dependency ✅ Faster (no API call, <5ms) ✅ Lower cost ($0 vs. $1/month)
Cons: ❌ Private key in application code - Risk of key leakage via git, logs, dumps ❌ Manual key rotation - Requires redeploying application ❌ No HSM protection - Private key stored in memory (vulnerable to attacks) ❌ No audit logging - Cannot track who signed what
Decision: REJECTED - Private key compromise = unlimited license generation.
Alternative 3: Third-Party HSM (AWS CloudHSM, Thales)
Architecture: Use AWS CloudHSM or Thales Luna HSM for signing.
Pros: ✅ Multi-cloud portability (not locked into GCP) ✅ FIPS 140-2 Level 3 compliant ✅ Physical HSM for maximum security
Cons: ❌ Higher cost - $2,000/month for AWS CloudHSM (vs. $1/month Cloud KMS) ❌ Complex setup - Requires VPN, custom client libraries ❌ Operational overhead - Manual HSM management, backups
Cost Comparison:
| Solution | Monthly Cost | Annual Cost |
|---|---|---|
| Cloud KMS | $1 | $12 |
| AWS CloudHSM | $2,000 | $24,000 |
| Thales Luna | $5,000 | $60,000 |
Decision: REJECTED - 2000× more expensive for same security level.
Alternative 4: Blockchain/Smart Contracts
Architecture: Store license signatures on Ethereum blockchain.
contract LicenseRegistry {
mapping(bytes32 => bool) public licenses;
function registerLicense(bytes32 licenseHash, bytes signature) public {
// Verify signature on-chain
// Store in immutable blockchain
}
}
Pros: ✅ Decentralized (no single point of failure) ✅ Immutable audit trail ✅ Public verifiability
Cons: ❌ Transaction fees - $2-50 per signature (vs. $0.000003 Cloud KMS) ❌ Latency - 10-60 seconds for transaction confirmation ❌ Complexity - Requires Ethereum node, wallet management ❌ Privacy - All licenses public on blockchain
Decision: REJECTED - Transaction fees and latency unacceptable for real-time licensing.
Implementation Notes
Cost Analysis
Monthly Costs (10,000 license activations):
| Component | Cost | Calculation |
|---|---|---|
| Key storage | $1.00 | 1 active key version |
| Signing operations | $0.03 | 10,000 ops / 10,000 × $0.03 |
| Total | $1.03 |
Annual Cost: $12.36
Cost per License: $0.000103 (negligible)
Security Hardening
1. Key Rotation
# Automatic 90-day rotation
resource "google_kms_crypto_key" "rsa_4096" {
rotation_period = "7776000s" # 90 days
# Keep old key versions for existing licenses
lifecycle {
prevent_destroy = true
}
}
2. IAM Restrictions
# Only license-server service account can sign
resource "google_kms_crypto_key_iam_member" "signer" {
crypto_key_id = google_kms_crypto_key.rsa_4096.id
role = "roles/cloudkms.signerVerifier"
member = "serviceAccount:license-server@project.iam.gserviceaccount.com"
}
# No human users can sign
# No export permission (private key stays in HSM)
3. Rate Limiting
# Rate limit signing operations (prevent abuse)
from django_ratelimit.decorators import ratelimit
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
@ratelimit(key='ip', rate='100/m', method='POST')
@api_view(['POST'])
@permission_classes([AllowAny])
def activate_license(request):
# ... signing logic ...
pass
4. Audit Logging
# Log all signing operations
import logging
logger.info(
"License signed",
extra={
"license_key": license.license_key,
"user_email": user.email,
"session_id": session_id,
"kms_key_version": key_version
}
)
Monitoring
Key Metrics:
from prometheus_client import Counter, Histogram
# Signing operations
kms_sign_total = Counter(
'kms_sign_operations_total',
'Total Cloud KMS signing operations',
['key_name', 'status']
)
# Signing latency
kms_sign_latency = Histogram(
'kms_sign_latency_seconds',
'Cloud KMS signing latency',
buckets=[0.01, 0.05, 0.1, 0.5, 1.0]
)
# Client verification
license_verify_total = Counter(
'license_verify_total',
'Total license verifications',
['result'] # valid, invalid, expired
)
Alerts:
- alert: HighKMSLatency
expr: histogram_quantile(0.95, kms_sign_latency_seconds) > 0.5
annotations:
summary: "Cloud KMS signing latency >500ms (p95)"
- alert: HighKMSFailureRate
expr: rate(kms_sign_operations_total{status="error"}[5m]) > 0.01
annotations:
summary: "Cloud KMS signing failures >1%"
Related ADRs
- ADR-001: Floating Licenses (defines offline grace periods requiring signed licenses)
- ADR-003: Check-on-Init Enforcement (license activation flow)
- ADR-008: Offline Grace Period Implementation (client-side signature verification)
- ADR-009: GCP Infrastructure (Cloud KMS deployment)
References
- Cloud KMS Documentation: https://cloud.google.com/kms/docs
- RSA-PSS Signature Scheme: https://datatracker.ietf.org/doc/html/rfc8017
- FIPS 140-2 Standard: https://csrc.nist.gov/publications/detail/fips/140/2/final
- Cryptography Best Practices: https://www.keylength.com/
Document Status: ✅ Complete Last Updated: 2025-11-30 Next Review: 2026-02-28 (quarterly) Owner: Security Team