· 5 years ago · Feb 21, 2021, 12:04 AM
1--- VeriCode - Easy code signing for ComputerCraft
2-- By JackMacWindows
3--
4-- @module vericode
5--
6-- Code signing uses encryption and hashes to easily verify a) that the sender of
7-- the code is trusted, and b) that the code hasn't been changed mid-transfer.
8-- VeriCode applies this concept to Lua code sent over Rednet to add a layer of
9-- security to Rednet. Just plainly receiving code from whoever sends it is
10-- dangerous, and invites the possibility of getting malware (in fact, I've made
11-- a virus that spreads through this method). Adding code signing ensures that
12-- any code received is safe and trusted.
13--
14-- Requires ecc library (pastebin get ZGJGBJdg ecc.lua)
15
16--[[ Basic usage:
171. Generate keypair files with vericode.generateKeypair
182. Copy the .key.pub file (NOT the standard .key file!!!) to each client that
19 needs to receive signed code
203. Require the API & load the key (.key on server, .key.pub on clients) - on the
21 server, make sure to store the key returned from loadKey as you'll need it to send
224. Call vericode.send to send a Lua script to a client computer
235. Call vericode.receive on the client to listen for code from the server (note
24 that it returns after receiving a function, so call it in an infinite loop if
25 you want it to always accept code)
26
27Example code:
28
29-- On server:
30local vericode = require "vericode"
31if not fs.exists("mykey.key") then
32 vericode.generateKeypair("mykey.key")
33 print("Please copy mykey.key.pub to the client computer.")
34 return
35end
36local key = vericode.loadKey("mykey.key")
37vericode.send(otherComputerID, "turtle.forward()", key, "turtleInstructions")
38
39-- On client:
40local vericode = require "vericode"
41vericode.loadKey("mykey.key.pub")
42while true do vericode.receive(true, "turtleInstructions") end
43
44--]]
45
46-- MIT License
47--
48-- Copyright (c) 2021 JackMacWindows
49--
50-- Permission is hereby granted, free of charge, to any person obtaining a copy
51-- of this software and associated documentation files (the "Software"), to deal
52-- in the Software without restriction, including without limitation the rights
53-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
54-- copies of the Software, and to permit persons to whom the Software is
55-- furnished to do so, subject to the following conditions:
56--
57-- The above copyright notice and this permission notice shall be included in all
58-- copies or substantial portions of the Software.
59--
60-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
61-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
62-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
63-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
64-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
65-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
66-- SOFTWARE.
67
68local function minver(version)
69 local res
70 if _CC_VERSION then res = version <= _CC_VERSION
71 elseif not _HOST then res = version <= os.version():gsub("CraftOS ", "")
72 else res = version <= _HOST:match("ComputerCraft ([0-9%.]+)") end
73 assert(res, "This program requires ComputerCraft " .. version .. " or later.")
74end
75
76minver "1.91.0"
77
78local expect = require "cc.expect".expect
79local ecc = require "ecc"
80
81local vericode = {}
82local keyStore = {}
83
84local b64str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
85
86local function base64encode(str)
87 local retval = ""
88 for s in str:gmatch "..." do
89 local n = s:byte(1) * 65536 + s:byte(2) * 256 + s:byte(3)
90 local a, b, c, d = bit32.extract(n, 18, 6), bit32.extract(n, 12, 6), bit32.extract(n, 6, 6), bit32.extract(n, 0, 6)
91 retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. b64str:sub(c+1, c+1) .. b64str:sub(d+1, d+1)
92 end
93 if #str % 3 == 1 then
94 local n = str:byte(-1)
95 local a, b = bit32.rshift(n, 2), bit32.lshift(bit32.band(n, 3), 4)
96 retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. "=="
97 elseif #str % 3 == 2 then
98 local n = str:byte(-2) * 256 + str:byte(-1)
99 local a, b, c, d = bit32.extract(n, 10, 6), bit32.extract(n, 4, 6), bit32.lshift(bit32.extract(n, 0, 4), 2)
100 retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. b64str:sub(c+1, c+1) .. "="
101 end
102 return retval
103end
104
105local function base64decode(str)
106 local retval = ""
107 for s in str:gmatch "...." do
108 if s:sub(3, 4) == '==' then
109 retval = retval .. string.char(bit32.bor(bit32.lshift(b64str:find(s:sub(1, 1)) - 1, 2), bit32.rshift(b64str:find(s:sub(2, 2)) - 1, 4)))
110 elseif s:sub(4, 4) == '=' then
111 local n = (b64str:find(s:sub(1, 1))-1) * 4096 + (b64str:find(s:sub(2, 2))-1) * 64 + (b64str:find(s:sub(3, 3))-1)
112 retval = retval .. string.char(bit32.extract(n, 10, 8)) .. string.char(bit32.extract(n, 2, 8))
113 else
114 local n = (b64str:find(s:sub(1, 1))-1) * 262144 + (b64str:find(s:sub(2, 2))-1) * 4096 + (b64str:find(s:sub(3, 3))-1) * 64 + (b64str:find(s:sub(4, 4))-1)
115 retval = retval .. string.char(bit32.extract(n, 16, 8)) .. string.char(bit32.extract(n, 8, 8)) .. string.char(bit32.extract(n, 0, 8))
116 end
117 end
118 return retval
119end
120
121vericode.base64 = {encode = base64encode, decode = base64decode}
122vericode.sha256 = ecc.sha256
123vericode.random = ecc.random
124vericode.ecc = ecc
125
126--- Generates a keypair for code signing.
127-- Outputs a pub/priv keypair at `path`, and a public-only key (for receivers) at `path`.pub.
128-- The generated key will be added to the store.
129-- @param path string The path to the file to generate.
130-- @return string pub The new public key.
131-- @return string priv The new private key. (Not required for this API, but might be useful otherwise.)
132function vericode.generateKeypair(path)
133 expect(1, path, "string")
134 local priv, pub = ecc.keypair(ecc.random.random())
135 pub, priv = base64encode(string.char(table.unpack(pub))), base64encode(string.char(table.unpack(priv)))
136 local file, err = fs.open(path, "w")
137 if not file then error("Could not open certificate file: " .. err, 2) end
138 file.write(textutils.serialize({
139 public = pub,
140 private = priv
141 }))
142 file.close()
143 file, err = fs.open(path .. ".pub", "w")
144 if not file then error("Could not open public certificate file: " .. err, 2) end
145 file.write(textutils.serialize({
146 public = pub
147 }))
148 file.close()
149 keyStore[pub] = {
150 public = pub,
151 private = priv
152 }
153 return pub, priv
154end
155
156--- Loads a key from disk. This can be a full keypair, or only a public key.
157-- @param path string The path to the key.
158-- @return string key The loaded public key.
159function vericode.loadKey(path)
160 expect(1, path, "string")
161 local file, err = fs.open(path, "r")
162 if not file then error("Could not open certificate file: " .. err, 2) end
163 local t = textutils.unserialize(file.readAll())
164 file.close()
165 if type(t) ~= "table" or t.public == nil then error("Invalid certificate file", 2) end
166 keyStore[t.public] = t
167 return t.public
168end
169
170--- Adds a public (and private if provided) key to the key store.
171-- @param pub string The public key to add.
172-- @param priv string|nil The private key to add, if desired.
173function vericode.addKey(pub, priv)
174 expect(1, pub, "string")
175 expect(2, priv, "string", "nil")
176 keyStore[pub] = {
177 public = pub,
178 private = priv
179 }
180end
181
182--- Compiles, dumps, and signs a Lua chunk.
183-- @param code string The Lua code to compile.
184-- @param key string The public or private key to use. The private key associated with this key must exist in the key store.
185-- @return string chunk A signed and compiled Lua chunk. This chunk can either be loaded with `load` here, or standard Lua `load`.
186function vericode.dump(code, key)
187 expect(1, code, "string")
188 expect(2, key, "string")
189 local pub, priv
190 if keyStore[key] then
191 if not keyStore[key].private then error("No private key associated with selected public key", 2) end
192 pub, priv = key, keyStore[key].private
193 else
194 for _,v in pairs(keyStore) do
195 if v.public == key then
196 if not v.private then error("No private key associated with selected public key", 2) end
197 pub, priv = key, v.private
198 break
199 elseif v.private == key then
200 pub, priv = v.public, key
201 break
202 end
203 end
204 end
205 if not pub or not priv then error("Could not find private key", 2) end
206 local fn, err = load(code, "=temp")
207 if not fn then error("Could not load chunk: " .. err, 2) end
208 local dump = string.dump(fn)
209 local size_t = dump:byte(9)
210 local chunk = dump:sub(19 + size_t)
211 local name = "=signed-chunk:" .. pub .. ":" .. base64encode(string.char(table.unpack(ecc.sign(base64decode(priv), chunk)))) .. "\0"
212 return dump:sub(1, 12) .. string.pack("I" .. size_t, #name) .. name .. chunk
213end
214
215--- Loads and verifies a previously signed code chunk.
216-- The public key associated with the chunk must be present in the key store.
217-- @param code string The code chunk to load.
218-- @param name string|nil The name of the chunk.
219-- @param _mode nil Ignored (for compatibility).
220-- @param env table|nil The environment to give the chunk.
221-- @return function|nil fn The returned function, or nil on error.
222-- @return nil|string err If an error occurred, the error message.
223function vericode.load(code, name, _mode, env)
224 expect(1, code, "string")
225 expect(2, name, "string", "nil")
226 expect(4, env, "table", "nil")
227 if code:sub(1, 5) ~= "\x1bLuaQ" then return nil, "Not a compiled Lua chunk" end
228 local size_t = code:byte(9)
229 local codename = code:sub(13 + size_t, 12 + size_t + string.unpack("I" .. size_t, code:sub(13, 12 + size_t)))
230 local chunk = code:sub(13 + size_t + #codename)
231 local key, sig = codename:match "^=signed%-chunk:([A-Za-z0-9+/]+=*):([A-Za-z0-9+/]+=*)\0$"
232 if not key or not sig then return nil, "Not signed" end
233 if not keyStore[key] then return nil, "Unrecognized key: " .. key end
234 if not ecc.verify(base64decode(key), chunk, {base64decode(sig):byte(1, -1)}) then return nil, "Invalid code signature" end
235 if name then code = code:sub(1, 12) .. string.pack("I" .. size_t, #name + 1) .. name .. "\0" .. chunk end
236 return load(code, name, "b", env)
237end
238
239--- Sends a signed code chunk over Rednet.
240-- @param recipient number The ID of the recipient.
241-- @param code string The code chunk to send.
242-- @param key string The key to use to sign the chunk.
243-- @param protocol string|nil The protocol to set, if desired.
244-- @return boolean ok Whether the message was sent.
245function vericode.send(recipient, code, key, protocol)
246 expect(1, recipient, "number")
247 expect(2, code, "string")
248 expect(3, key, "string")
249 expect(4, protocol, "string", "nil")
250 return rednet.send(recipient, vericode.dump(code, key), protocol)
251end
252
253--- Waits to receive a signed code chunk, and either returns the loaded function or the results from calling it.
254-- @param run boolean|nil Whether to run the code, or just return the function.
255-- @param filter string|nil The name of the protocol to listen for (nil for any).
256-- @param timeout number|nil The maximum amount of time to wait.
257-- @param name string|nil The name to give the loaded chunk (defaults to "=VeriCode chunk").
258-- @param env table|nil The environment to give the function.
259-- @return any res Either the loaded function, or the results from the function, or nil if the timeout was passed.
260function vericode.receive(run, filter, timeout, name, env)
261 expect(1, run, "boolean", "nil")
262 expect(2, filter, "string", "nil")
263 expect(3, timeout, "number", "nil")
264 expect(4, name, "string", "nil")
265 expect(5, env, "table", "nil")
266 local res = {n = 0}
267 local function receive()
268 while true do
269 local _, message = rednet.receive(filter)
270 if type(message) == "string" then
271 local fn = vericode.load(message, name or "=VeriCode chunk", nil, env)
272 if fn then
273 if run then res = table.pack(fn())
274 else res = {fn, n = 1} end
275 return table.unpack(res, 1, res.n)
276 end
277 end
278 end
279 end
280 if timeout then
281 parallel.waitForAny(receive, function() sleep(timeout) end)
282 return table.unpack(res, 1, res.n)
283 else return receive() end
284end
285
286return vericode
287