Skip to content

Policy Enforcement Guide

Target Audience: Security Engineers, Platform Administrators
Version: 3.0.0 Last Updated: December 2025


Overview

This guide explains how ANKASecure enforces cryptographic policies at runtime, with focus on hybrid key requirements and regulatory compliance (BSI, ANSSI, ETSI, NIST).


Policy Enforcement Architecture

Components

graph TD
    subgraph Admin-API
        A["Admin configures policy"] --> B["PostgreSQL:<br/>policy tables"]
        B --> C["PolicySyncService"]
        C --> D["Redis:<br/>policy cache"]
    end

    subgraph Core-API
        E["User request"] --> F["KeyGenerationService"]
        F --> G["AlgorithmAvailabilityPolicyHolder"]
        G --> H["5-Level Cache"]
        H --> I{"Cache Hit?"}
        I -->|L1| J["Caffeine<br/>in-memory"]
        I -->|L2| K["Redis"]
        I -->|L3| L["Local disk"]
        I -->|L4| M["Fail-closed<br/>deny all"]

        J & K & L & M --> N["AlgorithmAvailabilityPolicy"]
        N --> O["Evaluate request"]
        O --> P{"Permitted?"}
        P -->|Yes| Q["Generate key"]
        P -->|No| R["Throw AlgorithmNotAdmittedException (400 algorithm-not-admitted)"]
    end

    D -.sync.-> K

    style Q fill:#ccffcc
    style R fill:#ffcccc

Cache Hierarchy

ANKASecure uses a 5-level cache for policy resolution:

Level Storage TTL Purpose
L1 Caffeine (in-memory) 300s Ultra-fast lookups (<1ms)
L2 Redis 3600s Cross-instance consistency
L3 Local disk 24h Redis unavailability fallback
L4 Fail-closed N/A Emergency mode (deny all)
L5 Decision cache Per-tenant Caches permit/deny decisions

Performance: ~0.5ms average policy resolution time (L1 cache hit rate >95%)


Enforcement Flow for Single Keys

Step-by-Step Validation

sequenceDiagram
    participant User
    participant API as KeyGenerationService
    participant Policy as PolicyHolder
    participant Cache as 5-Level Cache
    participant Validator as AlgorithmValidator

    User->>API: POST /api/v3/admin/tenants/{tenantId}/keys<br/>{kty: ML-KEM, algorithm: ML-KEM-768}

    API->>Policy: Get policy for tenantId
    Policy->>Cache: Resolve policy
    Cache-->>Policy: AlgorithmAvailabilityPolicy

    Note over Policy: Policy loaded:<br/>BSI_HYBRID_ENFORCED<br/>requireComposite=true<br/>minSecurityLevel=3

    API->>Validator: Check algorithm allowed
    Validator-->>API: ✅ ML-KEM-768 in allowedFamilies

    API->>Validator: Check security level
    Validator-->>API: ✅ Level 3 ≥ minimum 3

    API->>Validator: Check hybrid requirement
    Note over Validator: ML-KEM is PQC?<br/>Yes ✅<br/>Hash-based signature?<br/>No ❌<br/>→ Hybrid REQUIRED
    Validator-->>API: ❌ DENY - Hybrid required

    API->>User: HTTP 400 algorithm-not-admitted, refusedBy requireComposite<br/>"... not admitted by the tenant algorithm policy (template ...);<br/>the refusing policy field is requireComposite ..."

Validation Logic

// Pseudocode for clarity
boolean validateKeyGeneration(Request request, Policy policy) {

    // Every refusal below is worded by AlgorithmAdmissionMessages: the algorithm, the policy that
    // judged it (the tenant's own, or the deployment policy it inherits, with its template) and the
    // refusing policy field. Each refusal is a 400 algorithm-not-admitted carrying the catalogue
    // algorithm and the refusing check (extensions.algorithm, extensions.refusedBy).

    // Step 1: Algorithm admitted? (explicit list, operations, category, family, level, standards)
    AlgorithmAdmission admission = admitSimple(request.alg, request.keyOps);
    if (!admission.admitted()) {
        throw new AlgorithmNotAdmittedException(
            detail(request.alg, admission.dimension(), admission.origin()),
            request.alg, refusedBy(admission.dimension()));
    }

    // Step 2: Security level ≥ minimum?
    int algorithmLevel = getSecurityLevel(request.alg);
    if (algorithmLevel < policy.minSecurityLevel) {
        throw new AlgorithmNotAdmittedException(
            detail(request.alg, MIN_SECURITY_LEVEL, policy.origin), request.alg, "minSecurityLevel");
    }

    // Step 3: Hybrid required? Post-quantum is the catalogue category POST_QUANTUM;
    // hash-based is SLH-DSA, XMSS (with XMSS^MT) and LMS (with HSS).
    if (policy.requireComposite && isPostQuantum(request.kty)) {
        if (!isHashBasedSignature(request.kty)) {
            // Worded by AlgorithmAdmissionMessages.requiresComposite: the algorithm, the policy in
            // force (tenant or inherited deployment, with its template) and the field requireComposite.
            throw new AlgorithmNotAdmittedException(
                requiresComposite(request.alg, policy.origin), request.alg, "requireComposite");
        }
    }

    return true;  // All validations passed
}

Enforcement Flow for Composite Keys

Step-by-Step Validation

sequenceDiagram
    participant User
    participant API as KeyGenerationService
    participant Policy as PolicyHolder
    participant Validator as CompositeValidator

    User->>API: POST /api/v3/key-management/keys<br/>{type: COMPOSITE, alg: X448+ML-KEM-768, kdf: CatKDF}

    API->>Validator: Check algorithm compatibility
    Note over Validator: X448 + ML-KEM-768<br/>Both Level 3<br/>Within 2 steps ✅
    Validator-->>API: ✅ Compatible

    API->>Policy: Get policy for tenantId
    Policy-->>API: ETSI_HYBRID_ENFORCED

    API->>Validator: CompositeAdmissionRule
    Note over Validator: families OKP, ML-KEM declared ✅<br/>weakest-link L3 in allowedLevels, ≥ 3 ✅<br/>pair standards include ETSI ✅<br/>mode COMPOSITE_KEM_COMBINE ✅<br/>kdf CatKDF in allowedKdfs ✅
    Validator-->>API: ✅ Admitted

    API->>User: HTTP 201 Created

Every policy dimension is enforced when a composite key is created

A composite create, import or rotation target, and the composite material of a SIMPLE → COMPOSITE transition, is judged by CompositeAdmissionRule against the tenant's effective policy, and the first failing dimension refuses the request with HTTP 400 application/problem+json (type algorithm-not-admitted) whose detail names the algorithm and that dimension and whose extensions.refusedBy names it as data (a fail-closed refusal names no dimension in detail and carries failClosed). An unknown composite is invalid-input with the invalid-composite-algorithm detail. allowedCompositeModes and allowedKdfs are part of that judgement, so they are refused per request, not only when a policy is saved.

allowedKdfs is judged last, and on composite KEM pairs only: the KDF the request names — or the pair's catalogue default, HKDF-SHA256, when it names none — must equal a member of the list exactly (no trim, no case folding). A template that names only HKDF-SHA256 therefore refuses a request naming HKDF-SHA384, HKDF-SHA512, CatKDF or CasKDF. A policy's allowedKdfs accepts only the five published tokens HKDF-SHA256, HKDF-SHA384, HKDF-SHA512, CatKDF, CasKDF; any other value, "" included, is refused with 400 when the policy is saved.

The Admin API also bounds a tenant policy by its deployment ceiling when either is saved (per dimension, with composite explicitAlgorithms entries judged by the same rule); see the declared residual in Policy Management. The evaluation order is in Algorithm-Availability Policy — composite (hybrid) admission.


Real-World Scenarios

Scenario 1: German Federal Agency

Configuration:

PUT /api/v3/admin/tenants/{tenantId}/policies/algorithm-availability
{"templateName": "BSI_HYBRID_ENFORCED"}

User Operations:

Request Result Reason
Generate ML-KEM-768 standalone ❌ DENIED BSI requires hybrid
Generate ML-KEM-512 standalone ❌ DENIED Level 1 < Level 3 minimum
Generate SLH-DSA standalone ✅ ALLOWED Hash-based exception
Generate brainpoolP384r1+ML-KEM-1024, HKDF-SHA256 ✅ ALLOWED Admitted on every dimension (both components BSI-recommended, BSI in its attribution)
Generate brainpoolP384r1+ML-KEM-1024, CatKDF ✅ ALLOWED CatKDF is in allowedKdfs
Generate brainpoolP384r1+ML-KEM-1024, CasKDF ❌ DENIED Not in allowedKdfs (HKDF-SHA256, CatKDF): BSI TR-02102-1 Table 2.1 does not recommend the cascade combiner
Generate brainpoolP384r1+ML-KEM-1024, HKDF-SHA384 ❌ DENIED Not in allowedKdfs (HKDF-SHA256, CatKDF), judged after every other dimension admitted the pair
Generate P-256+ML-KEM-768 ❌ DENIED Level 1 not in allowedLevels (L3, L5), checked before minSecurityLevel
Generate any RSA composite ❌ DENIED No RSA pairing exists in CompositeAlgorithmCatalogue; the request fails at algorithm resolution, before any policy check

Scenario 2: French Defense Contractor

Configuration:

PUT /api/v3/admin/tenants/{tenantId}/policies/algorithm-availability
{"templateName": "ANSSI_HYBRID_ENFORCED"}

User Operations:

Request Result Reason
Generate ML-DSA-65 standalone ❌ DENIED ANSSI requires hybrid
Generate P-384+ML-DSA-65 ✅ ALLOWED Admitted; a signature pair has no KDF, so allowedKdfs is not consulted
Generate Ed448+ML-DSA-65 ❌ DENIED OKP is not a declared family (no OKP algorithm or pair carries ANSSI), refused at allowedFamilies
Generate P-384+ML-KEM-768, CatKDF ✅ ALLOWED CatKDF is in allowedKdfs
Generate P-384+ML-KEM-768, CasKDF ✅ ALLOWED CasKDF is in allowedKdfs
Generate P-384+ML-KEM-768, HKDF-SHA512 ❌ DENIED Not in allowedKdfs: ANSSI lists HKDF-SHA256, CatKDF and CasKDF
Generate any FALCON composite ❌ DENIED No FALCON pairing exists in CompositeAlgorithmCatalogue; the request fails at algorithm resolution, before any policy check

Key Difference: ANSSI admits CasKDF, which the BSI template refuses.

Scenario 3: EU Telecommunications Operator

Configuration:

PUT /api/v3/admin/tenants/{tenantId}/policies/algorithm-availability
{"templateName": "ETSI_HYBRID_ENFORCED"}

User Operations:

Request Result Reason
Generate ML-KEM-1024 standalone ❌ DENIED ETSI requires hybrid
Generate X448+ML-KEM-768, CatKDF ✅ ALLOWED Admitted; CatKDF is in allowedKdfs
Generate X448+ML-KEM-768, CasKDF ✅ ALLOWED Admitted; CasKDF is in allowedKdfs
Generate COMPOSITE_SIGNATURE (Ed448+ML-DSA-65) ❌ DENIED ML-DSA is not a declared family, refused at allowedFamilies before allowedCompositeModes (which also names only COMPOSITE_KEM_COMBINE) is reached

Key Difference: ETSI focuses on KEM for key exchange (blocks signature modes).


Policy Synchronization

Update Propagation

sequenceDiagram
    participant Admin
    participant AdminAPI as Admin-API
    participant DB as PostgreSQL
    participant Sync as PolicySyncService
    participant Redis
    participant CoreAPI as Core-API Instances

    Admin->>AdminAPI: PUT /api/v3/admin/tenants/{id}/policies/algorithm-availability
    AdminAPI->>DB: Save policy
    DB-->>AdminAPI: Saved

    AdminAPI->>Sync: Trigger sync
    Sync->>Redis: Publish policy update
    Redis-->>Sync: Published

    Sync->>Redis: SET ankasecure:policy:algorithm:{tenantId}
    Redis-->>Sync: Stored (TTL 3600s)

    Redis->>CoreAPI: Pub/Sub notification
    CoreAPI->>CoreAPI: Invalidate Caffeine cache for tenant

    Note over CoreAPI: Next request will fetch<br/>updated policy from Redis

Propagation Time: ~1-2 seconds

Zero Downtime: Policy updates do not require service restart.


Troubleshooting

Common Errors

Error: "Algorithm ML-KEM-768 is not admitted by the tenant algorithm policy (template ...); the refusing policy field is requireComposite ..."

Cause: Tenant policy has requireComposite=true, user attempted standalone PQC key generation.

Solution:

# Instead of:
POST /api/v3/admin/tenants/{tenantId}/keys
{"kid": "k1", "kty": "ML-KEM", "algorithm": "ML-KEM-768"}

# Use:
POST /api/v3/admin/tenants/{tenantId}/keys
{
  "kid": "k1",
  "kty": "COMPOSITE",
  "algorithm": "brainpoolP384r1+ML-KEM-1024"
}

Error: "Algorithm (Level 1) below minimum security level 3"

Cause: Tenant policy enforces minimum Level 3, user requested Level 1 algorithm.

Solution: Use Level 3 or higher algorithms:

Level 1 (Blocked) Level 3 (Allowed) Level 5 (Allowed)
ML-KEM-512 ML-KEM-768 ML-KEM-1024
ML-DSA-44 ML-DSA-65 ML-DSA-87
RSA-2048 RSA-3072 RSA-4096

Error: "Algorithm P-384+ML-KEM-768 is not admitted by the tenant algorithm policy (template ANSSI_HYBRID_ENFORCED); the refusing policy field is allowedKdfs."

Cause: the request named a KDF the tenant's allowedKdfs does not list; the ANSSI template lists HKDF-SHA256, CatKDF and CasKDF.

Solution: Change KDF in request:

# Change from:
"kdf": "HKDF-SHA512"

# To:
"kdf": "HKDF-SHA256"

How to Check Active Policy

Query tenant's current policy:

GET /api/v3/admin/tenants/{tenantId}/policies/algorithm-availability

# Response:
{
  "templateName": "BSI_HYBRID_ENFORCED",
  "requireComposite": true,
  "allowedKdfs": ["HKDF-SHA256", "CatKDF"],
  "minSecurityLevel": 3,
  "regulatoryFramework": "BSI-TR-02102-1"
}

See Also

* restricted content — see higher-tier documentation