Billy Barrow 6 mesiacov pred
rodič
commit
9c4ecd8e2d

+ 26 - 0
src/cli/Cli.vala

@@ -19,6 +19,9 @@ public static int main(string[] args) {
     if(command == "info") {
         return info(args);
     }
+    if(command == "repository") {
+        return repository_main(args);
+    }
 
 
     usage();
@@ -76,4 +79,27 @@ private int info(string[] args) throws Error {
     }
 
     return 0;
+}
+
+public Invercargill.BinaryData calculate_file_checksum(string path) throws Error {
+
+    var stream = File.new_for_path(path).read();
+
+    var checksum = new Checksum(ChecksumType.SHA512);
+
+    const int increment = 1024 * 1024 * 128;
+    uint64 read = 0;
+    var to_read = stream.query_info("*").get_size();
+    while(read < to_read) {
+        var data = new uint8[uint64.min(increment, to_read - read)];
+        data.length = (int)stream.read(data);
+        checksum.update(data, data.length);
+        read += data.length;
+    }
+
+    var checksum_bytes = new uint8[ChecksumType.SHA512.get_length()];
+    size_t size = checksum_bytes.length;
+    checksum.get_digest(checksum_bytes, ref size);
+
+    return new Invercargill.BinaryData.from_byte_array(checksum_bytes);
 }

+ 129 - 0
src/cli/Repository.vala

@@ -0,0 +1,129 @@
+using Invercargill;
+
+private int repository_main(string[] args) {
+
+    if(args.length < 3) {
+        return repository_usage();
+    }
+
+    if(args[2] == "new") {
+        if(args.length != 4) {
+            return repository_usage();
+        }
+        return repository_new(args[3]);
+    }
+    if(args[2] == "build-list") {
+        return repository_build_list();
+    }
+    if(args[2] == "verify") {
+        if(args.length != 4) {
+            return repository_usage();
+        }
+        return repository_verify(args[3]);
+    }
+
+
+    return repository_usage();
+}
+
+private int repository_usage() {
+    printerr("USAGE:\n\tusm repository new <repository name>\n\tusm repository build-list\n\tusm repository verify repo.usmr\n");
+    return 255;
+}
+
+private int repository_new(string name) {
+    var public_key = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
+    var private_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
+    Sodium.Asymmetric.Signing.generate_keypair(public_key, private_key);
+
+    DirUtils.create("public", 0755);
+    
+    try {
+        FileUtils.set_data("public-key", public_key);
+        FileUtils.set_data("private-key", private_key);
+    
+        var repository = new Usm.Repository();
+        repository.key = new BinaryData.from_byte_array(public_key);
+        repository.name = name;
+        repository.url = @"file:///$(Environment.get_current_dir())/public";
+        repository.summary = "My new repository";
+    
+        var properties = Usm.Repository.get_mapper().map_from(repository);
+        var json = new InvercargillJson.JsonElement.from_properties(properties);
+        json.write_to_file(@"$name.usmr");
+    }
+    catch(Error e) {
+        printerr(@"Error: $(e.message)\n");
+        return e.code;
+    }
+
+    printerr("New repository created, populate the \"public\" directory with packages and run `usm repository build-list` in this directory to create a package list\n");
+    return 0;
+}
+
+private int repository_build_list() {
+    uint8[] public_key;
+    uint8[] private_key;
+    FileUtils.get_data("public-key", out public_key);
+    FileUtils.get_data("private-key", out private_key);
+
+    var output_stream = new DataOutputStream(File.new_build_filename("public", "PACKAGES.usml").replace(null, false, FileCreateFlags.REPLACE_DESTINATION));
+    var checksum = new Checksum(ChecksumType.SHA512);
+    var mapper = Usm.RepositoryListingEntry.get_mapper();
+
+    foreach(var file in directory("public")) {
+        var path = Path.build_filename("public", file);
+        if(!path.has_suffix(".usmc")) {
+            continue;
+        }
+
+        var manifest = new Usm.Manifest.from_package(path);
+
+        var entry = new Usm.RepositoryListingEntry() {
+            path = file,
+            manifest = manifest,
+            sha512sum = calculate_file_checksum(path)
+        };
+
+        var entry_properties = new InvercargillJson.JsonElement.from_properties(mapper.map_from(entry)).as<InvercargillJson.JsonObject>();
+        entry_properties.set("type", new InvercargillJson.JsonElement.from_element(new NativeElement<string>("usmc")));
+        var serialised = entry_properties.as_element().stringify(false);
+        checksum.update(serialised.data, serialised.data.length);
+        output_stream.put_string(@"$serialised\n");
+    }
+
+    var buffer = new uint8[ChecksumType.SHA512.get_length()];
+    size_t buffer_length = buffer.length;
+    checksum.get_digest(buffer, ref buffer_length);
+    var signed = Sodium.Asymmetric.Signing.sign(buffer, private_key);
+
+    var signature = new Usm.RepositoryListingSignature() {
+        key = new BinaryData.from_byte_array(public_key),
+        signature = new BinaryData.from_byte_array(signed)
+    };
+    var signature_entry = new PropertiesDictionary();
+    signature_entry.set("type", new NativeElement<string>("signatures"));
+    signature_entry.set("signatures", new NativeElement<Enumerable<Element>>(single(Usm.RepositoryListingSignature.get_mapper().map_from(signature)).to_elements()));
+
+    var serialised = new InvercargillJson.JsonElement.from_properties(signature_entry).stringify(false);
+    output_stream.put_string(serialised);
+
+    output_stream.close();
+    return 0;
+}
+
+
+private int repository_verify(string repository_file) {
+    var json = new InvercargillJson.JsonElement.from_file(repository_file);
+    var repository = Usm.Repository.get_mapper().materialise(json.as<InvercargillJson.JsonObject>());
+
+    var stream = File.new_for_uri(repository.url + "/PACKAGES.usml").read();
+    var listing = new Usm.RepositoryListing.from_stream(new DataInputStream(stream));
+
+    if(listing.valid_signature(repository.key)) {
+        printerr(@"Valid signature for \"$(repository.name)\" package listing.\n");
+        return 0;
+    }
+    printerr("Invalid signature.\n");
+    return 1;
+}

+ 1 - 0
src/cli/meson.build

@@ -1,6 +1,7 @@
 
 sources = files('Cli.vala')
 sources += files('Manifest.vala')
+sources += files('Repository.vala')
 
 deps = dependencies
 deps += usm_dep

+ 9 - 0
src/lib/Paths.vala

@@ -18,6 +18,9 @@ namespace Usm {
         public string shared_state { get; set; }
         public string sys_config { get; set; }
 
+        public string usm_repos_dir { get; set; }
+        public string usm_state_dir { get; set; }
+
         public void set_envs() {
             Environment.set_variable("DESTDIR", destination, true);
             Environment.set_variable("PREFIX", prefix, true);
@@ -95,6 +98,9 @@ namespace Usm {
             sbin = "sbin";
             shared_state = "com";
             sys_config = "etc";
+
+            usm_repos_dir = "/etc/usm/repos.d";
+            usm_state_dir = "/var/usm";
         }
 
         public Paths.usm_environ() {
@@ -114,6 +120,9 @@ namespace Usm {
             sbin = Environment.get_variable("USM_SBINDIR") ?? defaults.sbin;
             shared_state = Environment.get_variable("USM_SHAREDSTATEDIR") ?? defaults.shared_state;
             sys_config = Environment.get_variable("USM_SYSCONFIGDIR") ?? defaults.sys_config;
+
+            usm_repos_dir = Environment.get_variable("USM_REPOSDIR") ?? defaults.usm_repos_dir;
+            usm_state_dir = Environment.get_variable("USM_STATEDIR") ?? defaults.usm_state_dir;
         }
     }
 

+ 0 - 0
src/lib/Repository.vala → src/lib/Repository/Repository.vala


+ 17 - 0
src/lib/Repository/RepositoryClient.vala

@@ -0,0 +1,17 @@
+namespace Usm {
+
+    public delegate void RepositoryClientProgressCallback(string filename, int64 current, int64 total);
+
+    public abstract class RepositoryClient {
+
+        public Repository repository_config { get; protected set; }
+
+        public abstract void download_repository_listing(string path, RepositoryClientProgressCallback? callback = null);
+
+        public abstract void download_package(string path, RepositoryListingEntry package, RepositoryClientProgressCallback? callback = null);
+
+        public abstract void verify_package(string path, RepositoryListingEntry package, RepositoryClientProgressCallback? callback = null);
+
+    }
+
+}

+ 97 - 0
src/lib/Repository/RepositoryListing.vala

@@ -0,0 +1,97 @@
+using Invercargill;
+using InvercargillJson;
+
+namespace Usm {
+
+    public class RepositoryListing {
+
+        public Enumerable<RepositoryListingEntry> entries { get; set; }
+        public Enumerable<RepositoryListingSignature> signatures { get; set; }
+        public BinaryData checksum { get; set; }
+
+        public RepositoryListing.from_stream(DataInputStream stream) throws Error {
+            var entries = new Vector<RepositoryListingEntry>();
+            this.entries = entries;
+            var checksum = new Checksum(ChecksumType.SHA512);
+            while(true) {
+                var line = stream.read_line();
+                var json = new JsonElement.from_string(line).as<JsonObject>();
+                var type = json.get_string ("type");
+
+                if(type == "usmc") {
+                    checksum.update(line.data, line.data.length);
+                    entries.add(RepositoryListingEntry.get_mapper().materialise(json));
+                    continue;
+                }
+
+                if(type == "signatures") {
+                    var mapper = RepositoryListingSignature.get_mapper();
+                    signatures = json
+                        .get_array("signatures")
+                        .as_objects()
+                        .try_select<RepositoryListingSignature>(j => mapper.materialise(j))
+                        .unwrap_to_vector();
+
+                    var buffer = new uint8[ChecksumType.SHA512.get_length()];
+                    size_t buffer_length = buffer.length;
+                    checksum.get_digest(buffer, ref buffer_length);
+                    this.checksum = new BinaryData.from_byte_array(buffer);
+                    return;
+                }
+
+                if(type != null) {
+                    warning(@"Unknown entry type \"$type\" in repository listing, ignoring");
+                }
+                else {
+                    warning(@"Entry with no type found, ignoring");
+                }
+            }
+        }
+
+        public bool valid_signature(BinaryData key) {
+            var signature = signatures.first_or_default(s => s.key.equals(key));
+            if(signature == null) {
+                print("Not found\n");
+                return false;
+            }
+
+            var signed = Sodium.Asymmetric.Signing.verify(signature.signature.to_array(), key.to_array());
+            if(signed == null) {
+                print("Bad signature\n");
+                return false;
+            }
+
+            return checksum.equals(Convert.ate(signed));
+        }
+    }
+
+    public class RepositoryListingEntry {
+        public string path { get; set; }
+        public Manifest manifest { get; set; }
+        public BinaryData sha512sum { get; set; }
+
+
+        public static PropertyMapper<RepositoryListingEntry> get_mapper() {
+            return PropertyMapper.build_for<RepositoryListingEntry>(cfg => {
+                cfg.map<string>("path", o => o.path, (o, v) => o.path = v);
+                cfg.map_with<Manifest>("manifest", o => o.manifest, (o, v) => o.manifest = v, Manifest.get_mapper());
+                cfg.map<string>("sha512", o => o.sha512sum.to_base64(), (o, v) => o.sha512sum = new BinaryData.from_base64(v));
+                cfg.set_constructor(() => new RepositoryListingEntry());
+            });
+        }       
+    }
+
+    public class RepositoryListingSignature {
+        public BinaryData key { get; set; }
+        public BinaryData signature { get; set; }
+
+
+        public static PropertyMapper<RepositoryListingSignature> get_mapper() {
+            return PropertyMapper.build_for<RepositoryListingSignature>(cfg => {
+                cfg.map<string>("key", o => o.key.to_base64(), (o, v) => o.key = new BinaryData.from_base64(v));
+                cfg.map<string>("signature", o => o.signature.to_base64(), (o, v) => o.signature = new BinaryData.from_base64(v));
+                cfg.set_constructor(() => new RepositoryListingSignature());
+            });
+        }       
+    }
+}

+ 0 - 34
src/lib/RepositoryListing.vala

@@ -1,34 +0,0 @@
-using Invercargill;
-
-namespace Usm {
-
-    public class RepositoryListingEntry {
-        public string path { get; set; }
-        public Manifest manifest { get; set; }
-        public BinaryData sha512sum { get; set; }
-
-
-        public static PropertyMapper<RepositoryListingEntry> get_mapper() {
-            return PropertyMapper.build_for<RepositoryListingEntry>(cfg => {
-                cfg.map<string>("path", o => o.path, (o, v) => o.path = v);
-                cfg.map_with<Manifest>("manifest", o => o.manifest, (o, v) => o.manifest = v, Manifest.get_mapper());
-                cfg.map<string>("sha512", o => o.sha512sum.to_base64(), (o, v) => o.sha512sum = new BinaryData.from_base64(v));
-                cfg.set_constructor(() => new RepositoryListingEntry());
-            });
-        }       
-    }
-
-    public class RepositoryListingSignature {
-        public BinaryData key { get; set; }
-        public BinaryData signature { get; set; }
-
-
-        public static PropertyMapper<RepositoryListingSignature> get_mapper() {
-            return PropertyMapper.build_for<RepositoryListingSignature>(cfg => {
-                cfg.map<string>("key", o => o.key.to_base64(), (o, v) => o.key = new BinaryData.from_base64(v));
-                cfg.map<string>("signature", o => o.signature.to_base64(), (o, v) => o.signature = new BinaryData.from_base64(v));
-                cfg.set_constructor(() => new RepositoryListingSignature());
-            });
-        }       
-    }
-}

+ 6 - 3
src/lib/meson.build

@@ -10,8 +10,9 @@ sources += files('Licence.vala')
 sources += files('Paths.vala')
 sources += files('ResourceRef.vala')
 sources += files('Version.vala')
-sources += files('Repository.vala')
-sources += files('RepositoryListing.vala')
+sources += files('Repository/Repository.vala')
+sources += files('Repository/RepositoryListing.vala')
+sources += files('Repository/RepositoryClient.vala')
 
 
 dependencies = [
@@ -22,7 +23,9 @@ dependencies = [
     dependency('json-glib-1.0'),
     dependency('invercargill'),
     dependency('invercargill-json'),
-    dependency('libarchive')
+    dependency('libarchive'),
+    meson.get_compiler('vala').find_library('libsodium', dirs: vapi_dir),
+    meson.get_compiler('c').find_library('sodium'),
 ]
 
 usm = shared_library('usm', sources,

+ 237 - 0
src/vapi/libsodium.vapi

@@ -0,0 +1,237 @@
+/* Vala Bindings for LibSodium
+ * Copyright (c) 2020 Billy Barrow <billyb@pcthingz.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+ [CCode (cheader_filename = "sodium.h", lower_case_cprefix = "sodium_")]
+ namespace Sodium {
+ 
+   namespace Random {
+     [CCode (cname = "randombytes_SEEDBYTES")]
+     public const size_t SEED_BYTES;
+   
+     [CCode (cname = "randombytes_random")]
+     public uint32 random();
+   
+     [CCode (cname = "randombytes_uniform")]
+     public uint32 random_uniform(uint32 upper_bound);
+   
+     [CCode (cname = "randombytes_buf")]
+     public void random_bytes(uint8[] buffer);
+   
+     [CCode (cname = "randombytes_buf_deterministic")]
+     public void random_bytes_deterministic(uint8[] buffer, uint8[] seed);
+   }
+ 
+   namespace Symmetric {
+     [CCode (cname = "crypto_secretbox_KEYBYTES")]
+     public const size_t KEY_BYTES;
+ 
+     [CCode (cname = "crypto_secretbox_NONCEBYTES")]
+     public const size_t NONCE_BYTES;
+ 
+     [CCode (cname = "crypto_secretbox_MACBYTES")]
+     public const size_t MAC_BYTES;
+ 
+     [CCode (cname = "crypto_secretbox_keygen")]
+     private void key_gen([CCode (array_length = false)]uint8[] key);
+ 
+     public uint8[] generate_key() {
+       uint8[] key = new uint8[KEY_BYTES];
+       key_gen(key);
+       return key;
+     }
+ 
+     [CCode (cname = "crypto_secretbox_easy")]
+     private void secretbox(
+       [CCode (array_length = false)]uint8[] ciphertext,
+       uint8[] message,
+       [CCode (array_length = false)]uint8[] nonce,
+       [CCode (array_length = false)]uint8[] key
+     );
+ 
+     public uint8[] encrypt(uint8[] message, uint8[] key, uint8[] nonce)
+       requires (key.length == KEY_BYTES) 
+       requires (nonce.length == NONCE_BYTES)
+     {
+       // Initialise array for ciphertext
+       size_t ciphertext_size = MAC_BYTES + message.length;
+       uint8[] ciphertext = new uint8[ciphertext_size];
+ 
+       // Encrypt
+       secretbox(ciphertext, message, nonce, key);
+ 
+       // Return ciphertext
+       return ciphertext;
+     }
+ 
+     [CCode (cname = "crypto_secretbox_open_easy")]
+     private int secretbox_open(
+       [CCode (array_length = false)]uint8[] message,
+       uint8[] ciphertext,
+       [CCode (array_length = false)]uint8[] nonce,
+       [CCode (array_length = false)]uint8[] key
+     );
+ 
+     public uint8[]? decrypt(uint8[] ciphertext, uint8[] key, uint8[] nonce)
+       requires (ciphertext.length > MAC_BYTES)
+       requires (key.length == KEY_BYTES) 
+       requires (nonce.length == NONCE_BYTES)
+     {
+       // Initialise array for message
+       size_t message_size = ciphertext.length - MAC_BYTES;
+       uint8[] message = new uint8[message_size];
+ 
+       // Decrypt
+       int status = secretbox_open(message, ciphertext, nonce, key);
+ 
+       // Did it work?
+       if(status != 0) {
+         // No, return null
+         return null;
+       }
+ 
+       return message;
+     }
+   }
+   
+   namespace Asymmetric {
+ 
+     namespace Signing {
+ 
+         [CCode (cname = "crypto_sign_PUBLICKEYBYTES")]
+         public const size_t PUBLIC_KEY_BYTES;
+ 
+         [CCode (cname = "crypto_sign_SECRETKEYBYTES")]
+         public const size_t SECRET_KEY_BYTES;
+ 
+         [CCode (cname = "crypto_sign_BYTES")]
+         public const size_t MAX_HEADER_BYTES;
+ 
+         [CCode (cname = "crypto_sign_keypair")]
+         public void generate_keypair(
+             [CCode (array_length = false)]uint8[] public_key,
+             [CCode (array_length = false)]uint8[] secret_key)
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+             requires (secret_key.length == SECRET_KEY_BYTES);
+             
+         [CCode (cname = "crypto_sign")]
+         private void sign_message(
+             [CCode (array_length = false)] uint8[] signed_message,
+             out int signature_length,
+             uint8[] message,
+             [CCode (array_length = false)] uint8[] secret_key
+         );
+ 
+         public uint8[] sign(
+             uint8[] message,
+             uint8[] secret_key)
+             requires (secret_key.length == SECRET_KEY_BYTES)
+         {
+             int signature_length;
+             uint8[] signed_message = new uint8[MAX_HEADER_BYTES + message.length];
+             sign_message(signed_message, out signature_length, message, secret_key);
+             signed_message.resize(signature_length);
+ 
+             return signed_message;
+         }
+ 
+         [CCode (cname = "crypto_sign_open")]
+         private int sign_open(
+             [CCode (array_length = false)] uint8[] message,
+             out int message_length,
+             uint8[] signed_message,
+             [CCode (array_length = false)] uint8[] public_key
+         );
+ 
+         public uint8[]? verify(
+             uint8[] signed_message,
+             uint8[] public_key)
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+         {
+             int message_length;
+             uint8[] message = new uint8[signed_message.length];
+             if(sign_open(message, out message_length, signed_message, public_key) != 0) {
+                 return null;
+             }
+             message.resize(message_length);
+ 
+             return message;
+         }
+ 
+     }
+ 
+     namespace Sealing {
+ 
+         [CCode (cname = "crypto_box_PUBLICKEYBYTES")]
+         public const size_t PUBLIC_KEY_BYTES;
+ 
+         [CCode (cname = "crypto_box_SECRETKEYBYTES")]
+         public const size_t SECRET_KEY_BYTES;
+ 
+         [CCode (cname = "crypto_box_SEALBYTES")]
+         public const size_t HEADER_BYTES;
+ 
+         [CCode (cname = "crypto_box_keypair")]
+         public void generate_keypair(
+             [CCode (array_length = false)]uint8[] public_key,
+             [CCode (array_length = false)]uint8[] secret_key)
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+             requires (secret_key.length == SECRET_KEY_BYTES);
+ 
+         [CCode (cname = "crypto_box_seal")]
+         private void seal_message(
+             [CCode (array_length = false)] uint8[] ciphertext,
+             uint8[] message,
+             [CCode (array_length = false)] uint8[] public_key
+         );
+ 
+         public uint8[] seal(uint8[] message, uint8[] public_key)
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+         {
+             uint8[] ciphertext = new uint8[HEADER_BYTES + message.length];
+             seal_message(ciphertext, message, public_key);
+             return ciphertext;
+         }
+ 
+         [CCode (cname = "crypto_box_seal_open")]
+         private int seal_open(
+             [CCode (array_length = false)] uint8[] message,
+             uint8[] ciphertext,
+             [CCode (array_length = false)] uint8[] public_key,
+             [CCode (array_length = false)] uint8[] secret_key
+         );
+ 
+         public uint8[]? unseal(
+             uint8[] ciphertext,
+             uint8[] public_key,
+             uint8[] secret_key) 
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+             requires (secret_key.length == SECRET_KEY_BYTES)
+             requires (ciphertext.length > HEADER_BYTES)
+         {
+             uint8[] message = new uint8[ciphertext.length - HEADER_BYTES];
+             if(seal_open(message, ciphertext, public_key, secret_key) != 0){
+                 return null;
+             }
+             return message;
+         }
+         
+     }
+ 
+   }
+   
+ 
+ }