Skip to content

Geometric Proof System - Complete Guide

Version: 2.1 Last Updated: 2026-03-04 Architecture: Single-Tenant Rust

This document explains the Geometric Proof (GP) system - a challenge-response authentication mechanism used by Sawabona for ultra-robust license validation.

Note: The Geometric Proof system is tenant-agnostic and works identically in single-tenant and multi-tenant deployments. It validates licenses based on geometric transformations and HMAC signatures, independent of tenant context.


What is Geometric Proof?

Geometric Proof is NOT mathematically-based cryptography (like RSA or ECDSA) - it's a geometric manipulation system that generates proof vectors from geometric transformations. It provides a unique challenge-response protocol for license validation.

Core Concept

Client receives challenge: {seed, operations}
Client derives the bootstrap secret (HKDF over device_hash:code_hash:challenge_id)
Client derives WHICH FAMILY to draw from (derive_target_set, keyed to the secret)
Client selects ONE figure of that family (derive_figure_index, keyed to the secret)
Client folds the figure's descriptor into the final proof key (derive_proof_key)
Client builds the seeded single-figure tile (secret-derived GeoParams) and applies operations
Client extracts the canonical (quantized) GPV and HMACs it under the proof key
Server reproduces the same selection + key + tile and verifies the proof

System Architecture

Core Components

  1. sawabona-core/src/proof/ - Core GP algorithm (Rust)
  2. tile.rsbuild_tile() builds GP tiles from seed + figure collection + operations
  3. invariants.rscompute_gpv() extracts 12-float Geometric Proof Vector
  4. proof.rscompute_proof() / verify_proof() for HMAC-SHA256 proof over GPV

  5. Target Sets (sawabona-core/src/targets/sets/) - Geometric primitives for tile generation

  6. Greek System: 24 Greek letters with geometric properties (default)
  7. Zodiac System: 24 zodiac signs (12 Western + 12 Chinese animals)
  8. Geometric Forms: 24 geometric shapes in one unified collection (geometric_forms)

  9. API Endpoints (sawabona-api/src/handlers/geometric_proof.rs)

  10. POST /api/v1/geometric-proof/challenge - Generate a challenge
  11. POST /api/v1/licenses/validate - Verify proof, bind device, issue JWT

  12. Challenge Engine (sawabona-core/src/challenge/engine.rs)

  13. Generates random operations based on configuration
  14. Supports weighted operation selection
  15. Configurable challenge parameters (target set, ops range, TTL)

  16. GPV Dual-Proof (sawabona-api/src/handlers/geometric_proof.rs)

  17. When require_gpv = true on a product and gpv.enabled = true globally, both HMAC and GPV proofs must pass
  18. GPV proof is sent as gpv_proof field (base64-encoded) in the validation request
  19. Server independently rebuilds tile, computes GPV, and verifies via constant-time comparison

How It Works

Geometric primitives - Three target set families available:

  • Greek: 24 Greek letters (α, β, γ, δ, ... ω) — default
  • Zodiac: 24 signs (12 Western: Aries...Pisces + 12 Chinese: Rat...Pig)
  • Geometric Forms: 24 geometric shapes in a single unified collection (geometric_forms)
  • Circle, Square, Triangle, Rectangle, Pentagon, Hexagon, Octagon, 3 Ellipse variants, Heptagon, Nonagon, Decagon, Dodecagon, 3 Stars, Rhombus, Trapezoid, Parallelogram, Cross, Arrow, wide ellipse, tilted ellipse

Geometric transformations applied to tiles:

  • Rotation (45°, 90°, 135°, 180°, etc.)
  • Horizontal/vertical mirroring
  • Translation (X, Y, Z axis)
  • Scaling (optional)
  • Shearing (optional)

Invariant extraction - Geometric properties that remain constant:

  • Center of mass
  • Perimeter
  • Angle histogram
  • Stroke order parity

HMAC-based proof - Cryptographic signing of extracted invariants

Security Model

Security comes from:

  1. HMAC secret (derived from the license key via HKDF) — the sole cryptographic anchor. The algorithm and the geometric primitives are public (this is open-source software); their secrecy is not a security source. The HMAC secret is what an attacker lacks.
  2. Secret-derived key material (defense-in-depth) — the selected primitive is chosen via derive_figure_index, its canonical descriptor is folded into the proof key via derive_proof_key, and the figure's geometry is secret-parameterized via derive_geo_params + build_tile. So an attacker who knows the target set but not the license key cannot reconstruct the GPV. This layers additional secret-keyed material on top of the HMAC secret; it is not hard-math cryptography (no factoring or discrete-log hardness) and does not rely on algorithm obscurity.
  3. Single-use challenges — a challenge is consumed on the first verification attempt, regardless of whether that attempt succeeds. A failed proof cannot be replayed against the same challenge until its TTL expires; a fresh challenge must be requested for each attempt.
  4. Challenge expiration — challenges expire after TTL (default: 300 seconds; configurable via ttl_seconds).

Usage Examples

1. Generate a Challenge

Request:

curl -X POST http://localhost:8000/api/v1/geometric-proof/challenge \
  -H "Content-Type: application/json" \
  -d '{
    "license_key": "default-tenant_saw_prod_live_a1b2c3d4e5f6a7b8c9d0_f0e1",
    "product_slug": "prod",
    "device_hash": "sha256-device-fingerprint"
  }'

Response:

{
  "challenge_id": "chal_a1b2c3d4e5f6g7h8",
  "ttl": 300,
  "seed": 4827361,
  "ops": [
    { "type": "rotate", "angle": 90 },
    { "type": "mirror_h" },
    { "type": "translate", "dx": 1, "dy": -1, "dz": 0 }
  ],
  "server_sig": "ed25519-signature-base64"
}

2. Compute Proof (Client-Side, Rust)

use sawabona_core::crypto::{sha256_hex, derive_key, hmac_sha256};

// 1. Derive client secret
let code_hash = sha256_hex(b"geometric_proof_core");
let info = format!("{}:{}:{}", device_hash, code_hash, challenge_id);
let client_secret = derive_key(license_key.as_bytes(), info.as_bytes(), 32)?;

// 2. Build proof data
let mut proof_data = Vec::new();
proof_data.extend_from_slice(&seed.to_le_bytes());
proof_data.extend_from_slice(challenge_id.as_bytes());
for op in &ops {
    let op_sig = hmac_sha256(&client_secret, serde_json::to_string(op)?.as_bytes());
    proof_data.extend_from_slice(&op_sig);
}

// 3. Final HMAC proof
let proof = hmac_sha256(&client_secret, &proof_data);
let proof_b64 = base64::encode(&proof);

3. Validate License with Proof

Request:

curl -X POST http://localhost:8000/api/v1/licenses/validate \
  -H "Content-Type: application/json" \
  -d '{
    "license_key": "default-tenant_saw_prod_live_a1b2c3d4e5f6a7b8c9d0_f0e1",
    "proof": {
      "challenge_id": "chal_a1b2c3d4e5f6g7h8",
      "proof": "base64-encoded-hmac-proof"
    },
    "device_fingerprint": "sha256-device-fingerprint",
    "device_name": "My Workstation",
    "device_type": "desktop"
  }'

Response:

{
  "is_valid": true,
  "license_id": "uuid",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
  "message": "License valid"
}

Configuration

Built-in Defaults

The runtime ships with ChallengeConfig::production_default() (defined in sawabona-core/src/challenge/config.rs) as the single source of truth. The bundled YAML at sawabona-core/config/challenges/default.yaml mirrors those defaults, line for line:

version: "1.0"
challenge_type: "geometric"
# Informational. The family a challenge draws from is derived from the
# bootstrap secret, per challenge; this setting does not pin it.
target_set: "greek"

parameters:
  max_ops: 5
  min_ops: 2
  ttl_seconds: 300
  seed_range: [1000000, 9999999]
  proof_version: 1

operations:
  - type: "rotate"
    enabled: true
    weight: 30
    params:
      angles: [90, 180, 270]
  - type: "mirror_h"
    enabled: true
    weight: 20
  - type: "mirror_v"
    enabled: true
    weight: 20
  - type: "translate"
    enabled: true
    weight: 15
    params:
      dx_range: [-2, 2]
      dy_range: [-2, 2]
      dz_range: [0, 0]
  - type: "scale"
    enabled: true
    weight: 15
    params:
      scale_factors: [0.5, 1.0, 2.0]

Loading a Custom YAML

To override the built-in defaults, point the runtime at a YAML file via your TOML config (e.g. config/development.toml):

[challenge]
config_path = "config/challenges/default.yaml"

Loading is hard-fail: a missing or invalid YAML aborts startup rather than silently falling back. See sawabona-core/config/challenges/default.yaml for the complete shipped reference and advanced.yaml for a higher-friction profile (8 max ops, 60s TTL, 10M seed space, scale + shear enabled).

Storage Backend

Pending challenges are persisted in a backend selected by Config.challenge.storage. The choice is operational, not cryptographic — see sawabona-core/README.md#challenge-storage-backend for the full matrix:

Backend When to use
memory (default) Single-pod self-host. A background task purges expired entries every cleanup_interval_secs (default 540s = 9 min).
redis Required for ≥2 pods or rolling restarts. TTL handled natively by Redis.
[challenge.storage]
backend = "redis"
url = "redis://redis-host:6379"
key_prefix = "sawabona:challenge"   # optional

Environment Variables

The Environment::with_prefix("SAWABONA") source maps env vars to TOML keys with __ as the path separator. Useful overrides:

# Path to a challenge YAML override (maps to [challenge].config_path)
export SAWABONA_CHALLENGE__CONFIG_PATH=/etc/sawabona/challenge.yaml

# Background cleanup interval, seconds; 0 disables the task
export SAWABONA_CHALLENGE__CLEANUP_INTERVAL_SECS=60

# Global GPV kill-switch (per-product require_gpv still applies on top)
export SAWABONA_GPV__ENABLED=true

Storage backend selection is structural (tagged enum) and is best set in TOML rather than env vars.


Target Sets

Greek System (default)

24 Greek letters with geometric properties:

  • Vowels: α, ε, η, ι, ο, υ, ω
  • Consonants: β, γ, δ, ζ, θ, κ, λ, μ, ν, ξ, π, ρ, σ, τ, φ, χ, ψ

Each letter has geometric path definition, transformation rules, and invariant properties.

Default target set - Recommended for most use cases.

Zodiac System

24 zodiac signs (12 Western + 12 Chinese):

  • Western (12): Aries, Taurus, Gemini, Cancer, Leo, Virgo, Libra, Scorpio, Sagittarius, Capricorn, Aquarius, Pisces
  • Chinese (12): Rat, Ox, Tiger, Rabbit, Dragon, Snake, Horse, Goat, Monkey, Rooster, Dog, Pig

Each sign has SVG-like path definition, segment definitions, and transformation rules.

Alternative target set - Same cardinality as Greek (24 primitives).

Geometric Forms System

24 geometric shapes in a single unified geometric_forms target set, organized internally into 7 categories:

Index Figures Category
0–3 Circle, Square, Triangle, Rectangle BasicShapes
4–6 Pentagon, Hexagon, Octagon Polygons
7–9 Ellipse (horizontal, vertical, narrow) AdvancedShapes
10–13 Heptagon, Nonagon, Decagon, Dodecagon ExtendedPolygons
14–16 Star-5, Star-6, Star-8 Stars
17–19 Rhombus, Trapezoid, Parallelogram Quadrilaterals
20–23 Cross, Arrow, wide ellipse, tilted ellipse CompositeForms

geometric_forms is one of three families of 24. The other two are greek — the 24 letters of the alphabet — and zodiac — the 12 signs and the 12 planetary and alchemical symbols.

Nobody chooses which. The family is derived from the bootstrap secret, per challenge, the same way the figure index is. It is not in the challenge, not in the response, and not a parameter any client passes:

family = derive_target_set(bootstrap_secret, challenge_id)   # 1 of 3
index  = derive_figure_index(bootstrap_secret, challenge_id, 24)

Until 2026-08-25 the family was named in the challenge and sent to the client, so an observer reading the traffic knew which collection to look at and the unknown was one figure in 24. It is one in 72 now, and the observer learns neither half. All three families must hold the same number of figures: the index is taken over the collection, so a smaller family would shrink the unknown for every challenge that drew it.


Security Considerations

Strengths

  1. Unique Challenge-Response Protocol
  2. Different from standard JWT/OAuth flows
  3. Resistant to replay: a challenge is consumed on the first verification attempt regardless of success, so a captured proof cannot be re-submitted
  4. Binds license validation to geometric computation

  5. Client-Side Computation

  6. Offloads work to client (reduces server load)
  7. Proves the client holds the license-derived HMAC secret (anti-piracy) — the algorithm itself is public, so possessing it is not the gate
  8. Reduces network traffic

  9. Deterministic Invariants

  10. Same seed + operations always produce same GPV
  11. Enables reliable verification
  12. No randomness in computation

  13. Primitive-dependent proof

  14. The selected figure and its descriptor are bound into the proof key, and the figure's geometry is secret-parameterized, coupling the HMAC layer to the geometry and raising the bar for offline forgery attempts

Limitations

  1. Not Based on Hard Mathematical Problems
  2. RSA: Based on factoring large numbers (NP-hard)
  3. ECDSA: Based on discrete logarithm problem (NP-hard)
  4. GP: Based on geometric transformations (not proven hard)

  5. Security Depends on the HMAC Secret

  6. The HMAC secret (derived from the license key) is the primary security anchor. On top of it, the choice of primitive is secret-derived (derive_figure_index), the primitive's canonical descriptor is folded into the proof key (derive_proof_key), and the geometry itself is secret-parameterized (derive_geo_params + build_tile), so an attacker who knows the target set but not the license key cannot reconstruct the GPV. None of these mechanisms are based on hard mathematical problems (factoring, discrete log); they provide defense-in-depth and anti-reverse-engineering hardening, not cryptographic hardness. Algorithm obscurity is not a security feature.

Best Practices

  1. Always use HTTPS - Protect challenge and proof in transit
  2. Validate device fingerprints - Prevent device spoofing
  3. Enforce challenge expiration - Prevent replay attacks
  4. Rotate HMAC secrets - Periodically update license keys
  5. Monitor for attacks - Track failed proof attempts
  6. Use strong license keys - Ensure sufficient entropy in HMAC secrets

Protocol Version

There is a single canonical protocol, carried end-to-end in a proof_version field that is always 1:

  • secret-driven figure selection (derive_figure_index),
  • the selected primitive's descriptor folded into the proof key (derive_proof_key),
  • secret-parameterized seeded geometry (derive_geo_params + build_tile),
  • the canonical, 6-decimal-quantized GPV (compute_gpv) for cross-platform byte stability.

The proof_version field is retained as a forward-evolution hook so the protocol can change later without ambiguity, but today exactly one value is valid.

How proof_version flows

ChallengeConfig.parameters.proof_version   (config / YAML, validated == 1)
        ↓  generate_challenge()
Challenge.proof_version                     (stamped on the stored challenge)
        ↓  challenge endpoint
ChallengeResponse.proof_version             (returned to the client)
        ↓  client compute
LicenseClient::_compute_proof               (rejects anything but 1)
        ↓  prove endpoint
verify_proof                                (server-authoritative gate: == 1)

Server-authoritative

proof_version is read from the stored challenge, never from the client's proof submission. A client cannot alter the version it was issued. Both verify_proof and validate_license reject any stored version other than 1 with a 400 and a clear "please upgrade your client" message before any cryptographic work; ChallengeConfig::validate likewise rejects a non-1 proof_version at startup.

Upgrade path

To roll a future protocol version:

  1. Implement the new behavior behind a new arm in both the client (_compute_proof) and the server (verify_proof / validate_license), and widen the accepted-version checks.
  2. Bump proof_version in ChallengeParameters (config / YAML) and the config validator.
  3. Newly generated challenges carry the new version; clients that cannot compute it receive a 400 with the upgrade message rather than a silent proof mismatch.

Implementation Details (Rust)

Tile Building

// sawabona-core/src/proof/tile.rs
pub fn build_tile(
    seed: u64,
    collection: &dyn FigureCollection,
    ops: &[serde_json::Value],
    figure_index: usize,
    params: &GeoParams,
) -> Result<GeoProofTile>
// 1. Select the SINGLE figure at `figure_index`, where
//    figure_index = derive_figure_index(bootstrap_secret, challenge_id, collection.count()).
// 2. Emit its segments via Figure::segments_seeded(params) — secret-derived
//    sampling count, phase, vertex jitter and stroke order.
// 3. Apply operations (rotate, mirror, translate) sequentially.
// 4. Return tile with transformed segments.

Invariant Extraction

// sawabona-core/src/proof/invariants.rs
pub fn compute_gpv(segments: &[Segment]) -> GeometricProofVector
// Returns 12-float vector: center_x, center_y, perimeter, area,
// 6 angle histogram bins, stroke_parity, segment_count_norm

Proof Computation

// sawabona-core/src/proof/proof.rs
pub fn compute_proof(gpv: &GeometricProofVector, challenge_id: &str, client_secret: &[u8]) -> Result<Vec<u8>>
pub fn verify_proof(gpv: &GeometricProofVector, challenge_id: &str, client_secret: &[u8], proof: &[u8]) -> Result<bool>
// Uses HMAC-SHA256 with constant-time comparison

Troubleshooting

Challenge Generation Fails

Symptom: POST /api/v1/geometric-proof/challenge returns 400 error

Solutions:

  1. Verify license key is valid and active (not revoked/expired)
  2. Check product_slug matches the license's bound product
  3. Ensure target set is configured (greek, zodiac, or geometric_forms)
  4. Check challenge configuration is loaded

Proof Verification Fails

Symptom: POST /api/v1/licenses/validate returns 401 or proof mismatch

Solutions:

  1. Verify challenge hasn't expired (check TTL, default 300s)
  2. Verify HMAC secret derivation: HKDF(license_key, info=device_hash:code_hash:challenge_id)
  3. Verify operations are signed in the correct order
  4. Check device_fingerprint matches the one used for the challenge
  5. If require_gpv = true, ensure gpv_proof field is included

Target Set Not Found

Symptom: "Target set not registered"

Solutions:

  1. Check SAWABONA_TARGET_SET environment variable
  2. Verify target set name: greek, zodiac, geometric_forms, basic_shapes, etc.
  3. target_set in the configuration file is informational only — the family a challenge draws from is derived from the secret and cannot be pinned

References

  • API Reference: See reference/API-REFERENCE.md for endpoint details
  • How It Works: See GEOMETRIC-PROOF-EXPLAINED.md for worked examples with actual math
  • Greek Target Set: See targets/GREEK-TARGETS.md for Greek alphabet system details (default)
  • Zodiac Target Set: See targets/ZODIAC-TARGETS.md for zodiac system details (alternative)
  • Proof System: See sawabona-core/PROOF_SYSTEM.md for Rust implementation details
  • Testing: the protocol's own tests are in the repository — sawabona-core/tests/gpv_dual_proof_tests.rs, sawabona-core/tests/challenge_engine_tests.rs and sawabona-core/tests/e2e_gpv_proof_test.rs. They are what an independent implementation checks itself against.