|
root / src / core / signing.reef
signing.reef Reef 393 lines 11.8 KB
  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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
/******************************************************************************
               __               ____                __
              / /   ___  ____ _/ __/_____________ _/ /__
             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/

   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com

   Project: Zygaena
  Filename: signing.reef
   Authors: Chris Tusa <chris.tusa@leafscale.com>
   License: <see LICENSE file included with this source code>
Description: Ed25519 signing and verification for packages and repositories

******************************************************************************/

module core.signing

import core.str
import io.file
import io.dir
import io.path
import sys.process

export
    type KeyPair
    type SignResult
    type VerifyResult

    fn generate_keypair(name: string): SignResult
    fn sign_file(file_path: string, key_path: string): SignResult
    fn verify_file(file_path: string, sig_path: string, key_path: string): VerifyResult
    fn import_key(key_data: string, name: string): SignResult
    fn list_keys(keys: [KeyPair], max_count: int): int
    fn get_keyring_dir(): string
    fn get_trusted_keys_dir(): string

    // High-level verification functions
    fn verify_package(pkg_path: string, sig_path: string): VerifyResult
    fn verify_manifest(manifest_path: string): VerifyResult
    fn is_key_trusted(keyid: string): bool
end export

// Key pair information
type KeyPair = struct
    name: string
    keyid: string
    public_key: string
    private_key: string
    created: string
    trusted: bool
end KeyPair

// Result for signing operations
type SignResult = struct
    success: bool
    signature: string
    error: string
end SignResult

// Result for verification
type VerifyResult = struct
    success: bool
    valid: bool
    keyid: string
    error: string
end VerifyResult

// Get the keyring directory
fn get_keyring_dir(): string
    return "/etc/coral/keys"
end get_keyring_dir

// Create empty keypair
fn new_keypair(): KeyPair
    return KeyPair{ name: "", keyid: "", public_key: "", private_key: "", created: "", trusted: false }
end new_keypair

// Create error result
fn error_sign_result(msg: string): SignResult
    return SignResult{ success: false, signature: "", error: msg }
end error_sign_result

fn error_verify_result(msg: string): VerifyResult
    return VerifyResult{ success: false, valid: false, keyid: "", error: msg }
end error_verify_result

// Generate a new Ed25519 keypair
// Note: This requires an external tool (openssl or signify)
fn generate_keypair(name: string): SignResult
    let keyring = get_keyring_dir()

    // Ensure keyring directory exists
    if not dir.dir_exists(keyring)
        if not dir.create_dir_all(keyring)
            return error_sign_result("Failed to create keyring directory")
        end if
    end if

    let private_path = path.join_path(keyring, name + ".key")
    let public_path = path.join_path(keyring, name + ".pub")

    // Check if key already exists
    if file.fileExists(private_path)
        return error_sign_result("Key already exists: " + name)
    end if

    // Generate key using openssl
    let cmd = "openssl genpkey -algorithm ed25519 -out \"" + private_path + "\" && " +
              "openssl pkey -in \"" + private_path + "\" -pubout -out \"" + public_path + "\""

    let pid = process.process_spawn_shell(cmd)
    if pid < 0
        return error_sign_result("Failed to start key generation")
    end if

    let exit_code = process.process_wait(pid)
    if exit_code != 0
        return error_sign_result("Key generation failed")
    end if

    return SignResult{ success: true, signature: "", error: "" }
end generate_keypair

// Sign a file with Ed25519
fn sign_file(file_path: string, key_path: string): SignResult
    if not file.fileExists(file_path)
        return error_sign_result("File not found: " + file_path)
    end if

    if not file.fileExists(key_path)
        return error_sign_result("Key not found: " + key_path)
    end if

    let sig_path = file_path + ".sig"

    // Sign using openssl
    let cmd = "openssl pkeyutl -sign -inkey \"" + key_path + "\" -in \"" + file_path + "\" -out \"" + sig_path + "\""

    let pid = process.process_spawn_shell(cmd)
    if pid < 0
        return error_sign_result("Failed to start signing")
    end if

    let exit_code = process.process_wait(pid)
    if exit_code != 0
        return error_sign_result("Signing failed")
    end if

    return SignResult{ success: true, signature: sig_path, error: "" }
end sign_file

// Verify a file signature
fn verify_file(file_path: string, sig_path: string, key_path: string): VerifyResult
    if not file.fileExists(file_path)
        return error_verify_result("File not found: " + file_path)
    end if

    if not file.fileExists(sig_path)
        return error_verify_result("Signature not found: " + sig_path)
    end if

    if not file.fileExists(key_path)
        return error_verify_result("Key not found: " + key_path)
    end if

    // Verify using openssl
    let cmd = "openssl pkeyutl -verify -pubin -inkey \"" + key_path + "\" -in \"" + file_path + "\" -sigfile \"" + sig_path + "\""

    let pid = process.process_spawn_shell(cmd)
    if pid < 0
        return error_verify_result("Failed to start verification")
    end if

    let exit_code = process.process_wait(pid)

    if exit_code == 0
        return VerifyResult{ success: true, valid: true, keyid: "", error: "" }
    else
        return VerifyResult{ success: true, valid: false, keyid: "", error: "Signature verification failed" }
    end if
end verify_file

// Import a public key
fn import_key(key_data: string, name: string): SignResult
    let keyring = get_keyring_dir()

    if not dir.dir_exists(keyring)
        if not dir.create_dir_all(keyring)
            return error_sign_result("Failed to create keyring directory")
        end if
    end if

    let key_path = path.join_path(keyring, name + ".pub")

    if file.fileExists(key_path)
        return error_sign_result("Key already exists: " + name)
    end if

    if not file.writeFile(key_path, key_data)
        return error_sign_result("Failed to write key file")
    end if

    return SignResult{ success: true, signature: "", error: "" }
end import_key

// List all keys in keyring
fn list_keys(keys: [KeyPair], max_count: int): int
    let keyring = get_keyring_dir()

    if not dir.dir_exists(keyring)
        return 0
    end if

    mut entries: [string] = new [string](64)
    let entry_count = dir.list_dir(keyring, entries, 64)

    mut count = 0
    mut i = 0
    while i < entry_count and count < max_count
        let entry = entries[i]

        // Look for .pub files
        if str.ends_with(entry, ".pub")
            let name_len = str.length(entry) - 4
            let name = str.substring(entry, 0, name_len)

            mut kp = new_keypair()
            kp.name = name
            kp.trusted = true  // Local keys are trusted

            // Read public key
            let pub_path = path.join_path(keyring, entry)
            kp.public_key = file.readFile(pub_path)

            // Check for private key
            let priv_path = path.join_path(keyring, name + ".key")
            if file.fileExists(priv_path)
                kp.private_key = "(present)"
            end if

            keys[count] = kp
            count = count + 1
        end if

        i = i + 1
    end while

    return count
end list_keys

// Get the trusted keys directory
fn get_trusted_keys_dir(): string
    return "/var/lib/coral/trusted-keys"
end get_trusted_keys_dir

// Check if a key is in the trusted keys list
fn is_key_trusted(keyid: string): bool
    let trusted_dir = get_trusted_keys_dir()

    if not dir.dir_exists(trusted_dir)
        return false
    end if

    // Look for key file by keyid
    let key_path = path.join_path(trusted_dir, keyid + ".pub")
    if file.fileExists(key_path)
        return true
    end if

    // Also check the main keyring
    let keyring_path = path.join_path(get_keyring_dir(), keyid + ".pub")
    return file.fileExists(keyring_path)
end is_key_trusted

// Get path to a trusted key by keyid
fn get_trusted_key_path(keyid: string): string
    // Try trusted-keys directory first
    let trusted_dir = get_trusted_keys_dir()
    let trusted_path = path.join_path(trusted_dir, keyid + ".pub")
    if file.fileExists(trusted_path)
        return trusted_path
    end if

    // Fall back to main keyring
    let keyring_path = path.join_path(get_keyring_dir(), keyid + ".pub")
    if file.fileExists(keyring_path)
        return keyring_path
    end if

    return ""
end get_trusted_key_path

// Verify a package with any trusted key
fn verify_package(pkg_path: string, sig_path: string): VerifyResult
    if not file.fileExists(pkg_path)
        return error_verify_result("Package not found: " + pkg_path)
    end if

    if not file.fileExists(sig_path)
        return error_verify_result("Signature not found: " + sig_path)
    end if

    // Try to verify with each trusted key
    let trusted_dir = get_trusted_keys_dir()
    let keyring_dir = get_keyring_dir()

    // Collect all public keys from both directories
    mut all_keys: [string] = new [string](64)
    mut key_count = 0

    // Get keys from trusted-keys directory
    if dir.dir_exists(trusted_dir)
        mut entries: [string] = new [string](32)
        let entry_count = dir.list_dir(trusted_dir, entries, 32)

        mut i = 0
        while i < entry_count and key_count < 64
            if str.ends_with(entries[i], ".pub")
                all_keys[key_count] = path.join_path(trusted_dir, entries[i])
                key_count = key_count + 1
            end if
            i = i + 1
        end while
    end if

    // Get keys from keyring directory
    if dir.dir_exists(keyring_dir)
        mut entries: [string] = new [string](32)
        let entry_count = dir.list_dir(keyring_dir, entries, 32)

        mut i = 0
        while i < entry_count and key_count < 64
            if str.ends_with(entries[i], ".pub")
                let key_path = path.join_path(keyring_dir, entries[i])
                // Avoid duplicates
                mut is_dup = false
                mut j = 0
                while j < key_count
                    if str.equals(all_keys[j], key_path)
                        is_dup = true
                    end if
                    j = j + 1
                end while
                if not is_dup
                    all_keys[key_count] = key_path
                    key_count = key_count + 1
                end if
            end if
            i = i + 1
        end while
    end if

    if key_count == 0
        return error_verify_result("No trusted keys found")
    end if

    // Try each key
    mut i = 0
    while i < key_count
        let key_path = all_keys[i]
        let result = verify_file(pkg_path, sig_path, key_path)

        if result.success and result.valid
            // Extract keyid from path
            let filename = path.basename(key_path)
            let keyid = str.substring(filename, 0, str.length(filename) - 4)
            return VerifyResult{ success: true, valid: true, keyid: keyid, error: "" }
        end if

        i = i + 1
    end while

    return VerifyResult{ success: true, valid: false, keyid: "", error: "Signature does not match any trusted key" }
end verify_package

// Verify a repository manifest (repo.toml and repo.toml.sig)
fn verify_manifest(manifest_path: string): VerifyResult
    if not file.fileExists(manifest_path)
        return error_verify_result("Manifest not found: " + manifest_path)
    end if

    let sig_path = manifest_path + ".sig"
    if not file.fileExists(sig_path)
        return error_verify_result("Manifest signature not found: " + sig_path)
    end if

    return verify_package(manifest_path, sig_path)
end verify_manifest

end module