File size: 6,735 Bytes
a9a0235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
-- 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