Skip to content

How Geometric Proof Actually Works — Two Concrete Examples

What does it bring to Sawabona?

Geometric Proof (GP) is Sawabona's license validation protocol. Instead of just sending a license key to the server and getting back "yes/no", the client must prove it has the real algorithm and the real key by solving a geometric puzzle.

What it prevents:

  1. Simple key sharing — A stolen license key alone is useless. You also need the exact GP algorithm (embedded in the SDK) running on the registered device.
  2. Replay attacks — Each challenge is single-use and expires after the configured TTL (default 300 seconds).
  3. Man-in-the-middle — The proof is HMAC-signed; intercepting the challenge doesn't help without the secret derived from the license key.
  4. Reverse engineering — Even if someone decompiles the client, the proof is bound to device_hash + license_key + challenge_id, so a clone won't work on a different machine.

In short: GP turns license validation into a cryptographic handshake that binds together who you are (license key), where you are (device), and what you're running (code fingerprint).


The Full Flow (Simplified)

 CLIENT                                          SERVER
   |                                               |
   |  1. POST /challenge {license_key, product}    |
   |  -------------------------------------------> |
   |                                               |  Generate random seed
   |                                               |  Pick random operations
   |                                               |  Store challenge (single-use)
   |  2. {challenge_id, seed, ops, expires_at}     |
   |  <------------------------------------------- |
   |                                               |
   |  3. Client builds proof:                      |
   |     secret = HKDF(license_key,                |
   |                    device:code:challenge_id)   |
   |     for each op:                              |
   |       sig = HMAC(secret, op_json)             |
   |     proof = HMAC(secret, seed+id+all_sigs)    |
   |                                               |
   |  4. POST /prove {challenge_id, proof}         |
   |  -------------------------------------------> |
   |                                               |  Server rebuilds same proof
   |                                               |  Constant-time compare
   |                                               |  Delete challenge (no replay)
   |  5. {valid: true, token: "eyJ..."}            |
   |  <------------------------------------------- |
   |                                               |
   |  Client now has a JWT for API access          |

Example 1 — SIMPLE: The Square

Setup

The server generates this challenge:

{
  "challenge_id": "chal_a1b2c3d4e5f6g7h8",
  "seed": 42,
  "ops": [{ "op_type": "rotate", "params": { "angle": 90 } }],
  "expires_at": 1709400120
}

What the client does internally

Step A: Build the figure (Square)

A Square centered at origin with side=2 has 4 corners:

        (-1, 1) ●─────────● (1, 1)
                |           |
                |     ●     |        center = (0, 0)
                |   (0,0)   |
                |           |
       (-1,-1)  ●─────────● (1,-1)

4 segments:

  S0: (-1,-1) → ( 1,-1)   bottom edge     →  angle = 0°
  S1: ( 1,-1) → ( 1, 1)   right edge      →  angle = 90°
  S2: ( 1, 1) → (-1, 1)   top edge        →  angle = 180°
  S3: (-1, 1) → (-1,-1)   left edge       →  angle = 270°

Step B: Apply operation — Rotate 90°

The rotation matrix for 90° (π/2 radians):

  [ cos90  -sin90 ]   [ 0  -1 ]
  [ sin90   cos90 ] = [ 1   0 ]

Each point (x, y) becomes (-y, x):

  (-1,-1) → ( 1,-1)
  ( 1,-1) → ( 1, 1)
  ( 1, 1) → (-1, 1)
  (-1, 1) → (-1,-1)

Result after rotation:

       (-1, 1)  ●─────────● (1, 1)
                |           |
                |     ●     |        Same shape! (square is
                |   (0,0)   |        symmetric under 90° rotation)
                |           |
       (-1,-1)  ●─────────● (1,-1)

The square looks the same, but the segment order and direction changed:

  S0: ( 1,-1) → ( 1, 1)   was bottom, now right    →  angle = 90°
  S1: ( 1, 1) → (-1, 1)   was right, now top       →  angle = 180°
  S2: (-1, 1) → (-1,-1)   was top, now left        →  angle = 270°
  S3: (-1,-1) → ( 1,-1)   was left, now bottom     →  angle = 0°

Step C: Extract invariants (GPV — optional second proof factor)

When a product has require_gpv = true and the global config gpv.enabled = true, the GPV is computed and verified as a second proof factor alongside HMAC. Here's what the GPV looks like:

  center_of_mass  = (0.0, 0.0)     — unchanged by rotation
  perimeter       = 8.0            — unchanged (4 sides × 2.0)
  area            = 4.0            — unchanged
  angle_histogram = [0.25, 0, 0, 0.25, 0, 0.50]
                     0°    30° 60° 90° 120° 150-180°
  stroke_parity   = 0.0            — 4 segments, even
  segment_norm    = ln(4) + 1 ≈ 2.386

Step D: Compute the actual proof (what really gets sent)

  license_key = "saw_default-tenant_myapp_live_a1b2c3d4..."
  device_hash = "sha256_of_machine_fingerprint"
  code_hash   = SHA256("geometric_proof_core") = "7f3a..."

  1. Derive secret:
     info = "{device_hash}:{code_hash}:chal_a1b2c3d4e5f6g7h8"
     client_secret = HKDF-SHA256(key=license_key, info=info) → 32 bytes

  2. Build proof payload:
     proof_data  = 42_u64.to_le_bytes()                     →  8 bytes
     proof_data += "chal_a1b2c3d4e5f6g7h8".as_bytes()       → 24 bytes

  3. Sign the operation:
     op_json = '{"op_type":"rotate","params":{"angle":90}}'
     op_sig  = HMAC-SHA256(client_secret, op_json)           → 32 bytes
     proof_data += op_sig

  4. Final proof:
     proof = HMAC-SHA256(client_secret, proof_data)          → 32 bytes
     send  = base64(proof)                                   → 44 chars

The server does the exact same computation. If the 32-byte HMAC matches (constant-time comparison), the license is valid.


Example 2 — HARDEST: The 5-Pointed Star with 3 Operations

Setup

The server generates a complex challenge:

{
  "challenge_id": "chal_z9y8x7w6v5u4t3s2",
  "seed": 789012,
  "ops": [
    { "op_type": "rotate", "params": { "angle": 135 } },
    { "op_type": "mirror_h", "params": {} },
    { "op_type": "translate", "params": { "dx": 1, "dy": -1, "dz": 0 } }
  ],
  "expires_at": 1709400120
}

What the client does internally

Step A: Build the figure (5-pointed Star)

A Star-5 has 10 vertices alternating between outer radius (R=2) and inner radius (r=1), starting at the top (-90° = pointing up):

  Vertex layout (10 points, every 36°):

  V0 (outer, top):       ( 0.00,  2.00)    ← tip
  V1 (inner):            ( 0.59,  0.81)
  V2 (outer, right):     ( 1.90,  0.62)    ← tip
  V3 (inner):            ( 0.95, -0.31)
  V4 (outer, lower-R):   ( 1.18, -1.62)    ← tip
  V5 (inner):            ( 0.00, -1.00)
  V6 (outer, lower-L):   (-1.18, -1.62)    ← tip
  V7 (inner):            (-0.95, -0.31)
  V8 (outer, left):      (-1.90,  0.62)    ← tip
  V9 (inner):            (-0.59,  0.81)

Approximate shape (the * are the 5 outer tips, · are inner valleys):

                    *  V0
                   / \
                  /   \
             V9 ·     · V1
                |     |
         V8 *─·V7   V3·─* V2
                 \   /
                  \ /
               V6 *·V5·* V4

10 segments connecting V0→V1→V2→V3→...→V9→V0.

Each segment has a different angle and length. Outer→inner segments are shorter (≈1.18) than inner→outer segments (≈1.18) — they alternate between the two radii. The segments zigzag in and out.

Step B: Apply 3 operations sequentially

Operation 1: Rotate 135°

Matrix for 135° (3π/4 rad):

  cos(135°) = -0.707     sin(135°) = 0.707

  [ -0.707  -0.707 ]
  [  0.707  -0.707 ]

Every point (x,y) → (-0.707x - 0.707y, 0.707x - 0.707y):

  V0 ( 0.00,  2.00) → (-1.414, -1.414)
  V1 ( 0.59,  0.81) → (-0.990,  0.001)
  V2 ( 1.90,  0.62) → (-1.782,  0.907)
  V3 ( 0.95, -0.31) → (-0.452,  0.891)
  V4 ( 1.18, -1.62) → ( 0.311,  1.979)
  V5 ( 0.00, -1.00) → ( 0.707,  0.707)
  V6 (-1.18, -1.62) → ( 1.979,  0.311)
  V7 (-0.95, -0.31) → ( 0.891, -0.452)
  V8 (-1.90,  0.62) → ( 0.907, -1.782)
  V9 (-0.59,  0.81) → (-0.155, -1.000)

The star is now rotated 135° clockwise — the top tip now points to the lower-left.

                 · V2
                /
        V4 *  · V3
          / \/
     V5 ·   /\
        |  /  · V9
        * V0    \
       V6 ·     * V8
            \  /
             · V7

Operation 2: Mirror Horizontal (mirror_y — flip X)

mirror_y matrix: negate all X coordinates.

  [ -1   0 ]
  [  0   1 ]

Every point (x, y) → (-x, y):

  V0 (-1.414, -1.414) → ( 1.414, -1.414)
  V1 (-0.990,  0.001) → ( 0.990,  0.001)
  V2 (-1.782,  0.907) → ( 1.782,  0.907)
  V3 (-0.452,  0.891) → ( 0.452,  0.891)
  V4 ( 0.311,  1.979) → (-0.311,  1.979)
  V5 ( 0.707,  0.707) → (-0.707,  0.707)
  V6 ( 1.979,  0.311) → (-1.979,  0.311)
  V7 ( 0.891, -0.452) → (-0.891, -0.452)
  V8 ( 0.907, -1.782) → (-0.907, -1.782)
  V9 (-0.155, -1.000) → ( 0.155, -1.000)

The star is now a mirror image — like seeing it in a mirror placed along the Y axis.

Operation 3: Translate (+1, -1)

Shift everything right by 1, down by 1:

  V0 ( 1.414, -1.414) → ( 2.414, -2.414)
  V1 ( 0.990,  0.001) → ( 1.990, -0.999)
  V2 ( 1.782,  0.907) → ( 2.782, -0.093)
  V3 ( 0.452,  0.891) → ( 1.452, -0.109)
  V4 (-0.311,  1.979) → ( 0.689,  0.979)
  V5 (-0.707,  0.707) → ( 0.293, -0.293)
  V6 (-1.979,  0.311) → (-0.979, -0.689)
  V7 (-0.891, -0.452) → ( 0.109, -1.452)
  V8 (-0.907, -1.782) → ( 0.093, -2.782)
  V9 ( 0.155, -1.000) → ( 1.155, -2.000)

The whole star has shifted to the right and down:

                * V4
               / \
              /   \
          V5 ·     · V3
              \   /
          V6 *─\ /─* V2
              · V1
              |
          V7 ·     · V9
              \   /
          V8 *─\ /─* V0
                ·
      (centered around ~1.0, -1.0)

Step C: Compute the proof

Even though 3 operations produced complex geometry, the proof computation is elegantly simple — it doesn't care about the actual geometry at all!

  1. Derive secret (same as Example 1, different challenge_id):
     info = "{device_hash}:{code_hash}:chal_z9y8x7w6v5u4t3s2"
     client_secret = HKDF-SHA256(key=license_key, info=info) → 32 bytes

  2. Build proof payload:
     proof_data  = 789012_u64.to_le_bytes()                         →  8 bytes
     proof_data += "chal_z9y8x7w6v5u4t3s2".as_bytes()               → 24 bytes

  3. Sign EACH operation:
     op1_json = '{"op_type":"rotate","params":{"angle":135}}'
     op1_sig  = HMAC-SHA256(secret, op1_json)                        → 32 bytes

     op2_json = '{"op_type":"mirror_h","params":{}}'
     op2_sig  = HMAC-SHA256(secret, op2_json)                        → 32 bytes

     op3_json = '{"op_type":"translate","params":{"dx":1,"dy":-1,"dz":0}}'
     op3_sig  = HMAC-SHA256(secret, op3_json)                        → 32 bytes

     proof_data += op1_sig + op2_sig + op3_sig

  4. Final proof:
     proof = HMAC-SHA256(secret, proof_data)                         → 32 bytes
     send  = base64(proof)

Total payload signed: 8 + 24 + 32 + 32 + 32 = 128 bytes → one final 32-byte HMAC.


Why the Geometry Matters (Even Though the Proof is HMAC-Based)

You might wonder: if the proof is just HMAC over the operation JSON, why bother with geometry at all?

Three reasons:

  1. Anti-reverse-engineering layer. The build_tile + extract_invariants code exists in the client SDK. An attacker decompiling the SDK sees complex geometric math (affine transforms, angle histograms, shoelace formula) and must understand all of it to know what matters and what doesn't. This makes the attack surface harder to map.

  2. Dual-proof factor (GPV). The GPV (Geometric Proof Vector) is now wired as an optional second proof mechanism. When a product has require_gpv = true and the server config has gpv.enabled = true, the client must send both the HMAC proof AND the GPV proof. The server independently rebuilds the tile, computes the GPV, and verifies the client's GPV proof via constant-time comparison. Both proofs must pass for validation to succeed.

  3. Tile-based challenge binding. The build_tile function is deterministic (ChaCha8 RNG from seed). The server can independently rebuild the exact same tile and verify geometric properties if needed — e.g., "the transformed star's center of mass is at (1.0, -1.0)" — as an additional server-side check layered on top of HMAC.


Summary Table

Aspect Example 1: Square Example 2: Star-5
Figure 4 corners, 4 segments 10 vertices, 10 segments
Operations 1 (rotate 90°) 3 (rotate 135° + mirror_h + translate)
Geometric complexity Low — square is symmetric High — zigzag edges, asymmetric result
Proof computation Identical mechanism Identical mechanism
Proof size 44 chars (base64 of 32 bytes) 44 chars (same)
Security HMAC binds license+device+challenge Same

The geometric complexity of the target and operations does NOT affect proof security — that comes entirely from HMAC-SHA256 + HKDF key derivation. The geometry adds obfuscation depth and future extensibility.