Skip to main content

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:

  1. Offline License Validation - Clients verify licenses locally without internet
  2. Tamper Detection - Prevent license modification or key forgery
  3. Grace Period Enforcement - Securely validate offline_expires_at timestamps
  4. 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:

SolutionMonthly CostAnnual 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):

ComponentCostCalculation
Key storage$1.001 active key version
Signing operations$0.0310,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%"

  • 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


Document Status: ✅ Complete Last Updated: 2025-11-30 Next Review: 2026-02-28 (quarterly) Owner: Security Team