Intent-V / clone-gate /src /CloneGate.idr
SNAPKITTYWEST's picture
Add Clone Gate: type-level license gate in Idris 2 (covenant-licensed)
a9a0235 verified
Raw History Blame Contribute Delete
6.74 kB
-- SPDX-License-Identifier: LicenseRef-Sovereign-Leviathan-SL-AGPL3-001
-- Copyright (c) 2026 Ahmad Ali Parr
module CloneGate
import Data.Vect
import Data.Bits
import Decidable.Equality
%default total
------------------------------------------------------------------------
-- 1. Bytes, AES-style state, fingerprints
------------------------------------------------------------------------
public export
Byte : Type
Byte = Bits8
public export
AESState : Type
AESState = Vect 4 (Vect 4 Byte)
public export
Fingerprint : Type
Fingerprint = Vect 16 Byte
-- | Toy S-box. Real AES uses a fixed 256-byte permutation; here we
-- | just add 1 so the transformation is total and testable.
export
sbox : Byte -> Byte
sbox b = b + 1
export
subBytes : AESState -> AESState
subBytes = map (map sbox)
-- | XOR the key into the state, cell by cell.
export
addRoundKey : AESState -> AESState -> AESState
addRoundKey = zipWith (zipWith xor)
-- | One round of the toy cipher.
export
aesRound : AESState -> AESState -> AESState
aesRound st k = addRoundKey (subBytes st) k
-- | Toy "decryption": the inverse XOR.
export
decryptChallenge : AESState -> AESState -> AESState
decryptChallenge k ct = addRoundKey ct k
-- | Flatten the 4x4 state into a 16-byte fingerprint.
-- | The type-checker reduces 4 + (4 + (4 + 4)) to 16 by computation.
export
flatten : AESState -> Fingerprint
flatten (r1 :: r2 :: r3 :: r4 :: Nil) = r1 ++ r2 ++ r3 ++ r4
-- | The cryptographic gate: does the decrypted challenge match
-- | the fingerprint bound to the license?
export
verifyCrypto : Fingerprint -> AESState -> AESState -> Bool
verifyCrypto fp key challenge =
flatten (decryptChallenge key challenge) == fp
------------------------------------------------------------------------
-- 2. License status and the type-level proof
------------------------------------------------------------------------
public export
data LicenseStatus = Trial | Expired | Valid
-- | The single-constructor proof type. It is inhabited ONLY for
-- | `Valid`, which is what makes the whole scheme type-level: the
-- | compiler can only find a `LicenseProof Valid` if the license
-- | really is valid.
export
data LicenseProof : LicenseStatus -> Type where
ValidProof : LicenseProof Valid
public export
data LicenseToken : Type where
MkToken : (status : LicenseStatus)
-> (fp : Fingerprint)
-> LicenseToken
-- Decidable equality on LicenseStatus (illustrative, and needed by
-- some downstream consumers).
noTrialValid : Trial = Valid -> Void
noTrialValid Refl impossible
noExpiredValid : Expired = Valid -> Void
noExpiredValid Refl impossible
export
decEqStatus : (a, b : LicenseStatus) -> Dec (a = b)
decEqStatus Trial Trial = Yes Refl
decEqStatus Trial Expired = No (\case Refl impossible)
decEqStatus Trial Valid = No noTrialValid
decEqStatus Expired Trial = No (\case Refl impossible)
decEqStatus Expired Expired = Yes Refl
decEqStatus Expired Valid = No noExpiredValid
decEqStatus Valid Trial = No (negEqSym noTrialValid)
decEqStatus Valid Expired = No (negEqSym noExpiredValid)
decEqStatus Valid Valid = Yes Refl
export
DecEq LicenseStatus where
decEq = decEqStatus
------------------------------------------------------------------------
-- 3. The license decider -- the ONLY place ValidProof is constructed
------------------------------------------------------------------------
-- | Takes a token, a key, and a challenge response. Returns a
-- | `Just proof` if and only if (a) the status is `Valid` and (b)
-- | the AES gate verifies the challenge against the token's
-- | fingerprint.
export
decideLicense : LicenseToken
-> AESState
-> AESState
-> Maybe (LicenseProof Valid)
decideLicense (MkToken Trial _) _ _ = Nothing
decideLicense (MkToken Expired _) _ _ = Nothing
decideLicense (MkToken Valid fp) key challenge =
case verifyCrypto fp key challenge of
True => Just ValidProof
False => Nothing
------------------------------------------------------------------------
-- 4. The state-indexed, linear clone gate
------------------------------------------------------------------------
public export
data GateState = Locked | Unlocked | Spent
-- | A clone gate is indexed by its state. External code cannot
-- | fabricate a `CloneGate Unlocked` without going through
-- | `unlockGate`.
export
data CloneGate : GateState -> Type where
LockedGate : (gateId : Int) -> CloneGate Locked
UnlockedGate : (gateId : Int) -> CloneGate Unlocked
SpentGate : (gateId : Int) -> CloneGate Spent
-- | Unlocking consumes the locked gate linearly and requires the
-- | erased `LicenseProof Valid` witness. The `0` multiplicity means
-- | the proof has no runtime representation.
export
unlockGate : (0 _ : LicenseProof Valid)
-> (1 g : CloneGate Locked)
-> CloneGate Unlocked
unlockGate _ (LockedGate i) = UnlockedGate i
-- | Cloning consumes the unlocked gate linearly and yields a payload
-- | plus a spent gate, which must then be discarded.
export
cloneGate : (1 g : CloneGate Unlocked)
-> (Vect 4 Byte, CloneGate Spent)
cloneGate (UnlockedGate i) = (replicate 4 0, SpentGate i)
-- | The spent gate must be explicitly discarded, closing the linear
-- | protocol.
export
discardGate : (1 g : CloneGate Spent) -> ()
discardGate (SpentGate _) = ()
------------------------------------------------------------------------
-- 5. The main gate flow
------------------------------------------------------------------------
export
gateFlow : LicenseToken
-> AESState
-> AESState
-> Maybe (Vect 4 Byte)
gateFlow token key challenge =
case decideLicense token key challenge of
Nothing => Nothing
Just prf =>
let gate = LockedGate 0
unlocked = unlockGate prf gate
(payload, spent) = cloneGate unlocked
in case discardGate spent of
() => Just payload
------------------------------------------------------------------------
-- 6. A tiny smoke test
------------------------------------------------------------------------
export
dummyKey : AESState
dummyKey = replicate 4 (replicate 4 0)
export
dummyChallenge : AESState
dummyChallenge = replicate 4 (replicate 4 0)
export
dummyFP : Fingerprint
dummyFP = flatten (decryptChallenge dummyKey dummyChallenge)
export
validToken : LicenseToken
validToken = MkToken Valid dummyFP
export
expiredToken : LicenseToken
expiredToken = MkToken Expired dummyFP
export
testValid : Maybe (Vect 4 Byte)
testValid = gateFlow validToken dummyKey dummyChallenge
export
testExpired : Maybe (Vect 4 Byte)
testExpired = gateFlow expiredToken dummyKey dummyChallenge