From 016986f7dcc3f0f3d047be9160ea56d671a47ca2 Mon Sep 17 00:00:00 2001 From: Bence Szigeti Date: Mon, 11 Sep 2023 12:06:38 +0200 Subject: [PATCH] Initial Crypto module in Ziglang --- Makefile.conf.template | 3 +- modules/crypto/.gitignore | 3 + modules/crypto/Makefile | 10 +++ modules/crypto/build.zig | 44 ++++++++++++ modules/crypto/src/log.zig | 46 ++++++++++++ modules/crypto/src/main.zig | 123 ++++++++++++++++++++++++++++++++ modules/crypto/src/opensips.zig | 84 ++++++++++++++++++++++ scripts/build/do_build.sh | 2 +- test.cfg | 22 ++++++ 9 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 modules/crypto/.gitignore create mode 100644 modules/crypto/Makefile create mode 100644 modules/crypto/build.zig create mode 100644 modules/crypto/src/log.zig create mode 100644 modules/crypto/src/main.zig create mode 100644 modules/crypto/src/opensips.zig create mode 100644 test.cfg diff --git a/Makefile.conf.template b/Makefile.conf.template index fb7d986cd65..5abd473f952 100644 --- a/Makefile.conf.template +++ b/Makefile.conf.template @@ -66,8 +66,9 @@ #xml= Introduces a new type of variable that provides both serialization and de-serialization from XML format. | XML library, libxml2-dev #xmpp= Gateway between OpenSIPS and a jabber server. It enables the exchange of IMs between SIP clients and XMPP(jabber) clients. | parsing/building XML files, typically libexpat1-devel #uuid= UUID generator | uuid-dev +#crypto= Cryptography | -exclude_modules?= aaa_diameter aaa_radius auth_jwt b2b_logic_xml cachedb_cassandra cachedb_couchbase cachedb_memcached cachedb_mongodb cachedb_redis carrierroute cgrates compression cpl_c db_berkeley db_http db_mysql db_oracle db_perlvdb db_postgres db_sqlite db_unixodbc dialplan emergency event_rabbitmq event_kafka h350 httpd identity jabber json ldap lua mi_xmlrpc_ng mmgeoip osp perl pi_http presence presence_dialoginfo presence_mwi presence_xml presence_dfks proto_sctp proto_tls proto_wss pua pua_bla pua_dialoginfo pua_mi pua_usrloc pua_xmpp python regex rabbitmq rabbitmq_consumer rest_client rls siprec sngtc snmpstats stir_shaken tls_mgm tls_openssl tls_wolfssl uuid xcap xcap_client xml xmpp +exclude_modules?= aaa_diameter aaa_radius auth_jwt b2b_logic_xml cachedb_cassandra cachedb_couchbase cachedb_memcached cachedb_mongodb cachedb_redis carrierroute cgrates compression cpl_c db_berkeley db_http db_mysql db_oracle db_perlvdb db_postgres db_sqlite db_unixodbc dialplan emergency event_rabbitmq event_kafka h350 httpd identity jabber json ldap lua mi_xmlrpc_ng mmgeoip osp perl pi_http presence presence_dialoginfo presence_mwi presence_xml presence_dfks proto_sctp proto_tls proto_wss pua pua_bla pua_dialoginfo pua_mi pua_usrloc pua_xmpp python regex rabbitmq rabbitmq_consumer rest_client rls siprec sngtc snmpstats stir_shaken tls_mgm tls_openssl tls_wolfssl uuid xcap xcap_client xml xmpp crypto include_modules?= diff --git a/modules/crypto/.gitignore b/modules/crypto/.gitignore new file mode 100644 index 00000000000..3687b4acf24 --- /dev/null +++ b/modules/crypto/.gitignore @@ -0,0 +1,3 @@ +zig-cache/ +zig-out/ +*.so diff --git a/modules/crypto/Makefile b/modules/crypto/Makefile new file mode 100644 index 00000000000..07176e1e7db --- /dev/null +++ b/modules/crypto/Makefile @@ -0,0 +1,10 @@ +include ../../Makefile.defs + +NAME=crypto.so + +$NAME: + -@MAKE_DEFS="$$(echo "$${DEFS}" | xargs) " \ + zig build -Doptimize=ReleaseFast + -@cp zig-out/lib/libcrypto.so ${NAME} + +include ../../Makefile.modules diff --git a/modules/crypto/build.zig b/modules/crypto/build.zig new file mode 100644 index 00000000000..8bbbef21a48 --- /dev/null +++ b/modules/crypto/build.zig @@ -0,0 +1,44 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addSharedLibrary(.{ + .name = "crypto", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + lib.linkLibC(); + lib.addIncludePath(.{ .path = "../../" }); + lib.strip = true; + + var defs = std.mem.tokenizeSequence(u8, std.os.getenv("MAKE_DEFS").?, "-D"); + while (defs.next()) |def| { + var kv = std.mem.tokenize(u8, def, "="); + const key = kv.next().?; + const value = kv.next(); + if (value == null) { + lib.defineCMacro(key[0 .. key.len - 1], ""); + continue; + } + const trim: usize = if (value.?[0] == '\'') 1 else 0; + lib.defineCMacro(key, value.?[trim..(value.?.len - trim - 1)]); + } + + b.installArtifact(lib); + + const main_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_main_tests = b.addRunArtifact(main_tests); + + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&run_main_tests.step); +} diff --git a/modules/crypto/src/log.zig b/modules/crypto/src/log.zig new file mode 100644 index 00000000000..e120e01a46c --- /dev/null +++ b/modules/crypto/src/log.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +pub const c = @cImport({ + @cInclude("sr_module.h"); +}); + +// Fixed C-to-Zig dp_time: +fn dp_time() callconv(.C) [*c]u8 { + var ltime: c.time_t = undefined; + _ = c.time(<ime); + _ = c.ctime_r(<ime, c.ctime_buf); + c.ctime_buf[19] = 0; + return c.ctime_buf + 4; +} + +fn logLevelText(comptime level: std.log.Level) []const u8 { + return switch (level) { + .err => "ERROR", + .warn => "WARNING", + .info => "INFO", + .debug => "DEBUG", + }; +} + +pub fn logger( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + const prefix = "{s} [{d}] " ++ comptime logLevelText(level) ++ ":" ++ @tagName(scope) ++ ": "; + var log_buf: [4096:0]u8 = undefined; + const log = std.fmt.bufPrint(&log_buf, prefix ++ format ++ "\n\x00", .{ dp_time(), c.dp_my_pid() } ++ args) catch trimmed: { + std.debug.assert(log_buf.len > 5); + const trim_text: []const u8 = "...\n"; + @memcpy(log_buf[log_buf.len - trim_text.len ..], trim_text); + break :trimmed &log_buf; + }; + const opensips_level = switch (level) { + std.log.Level.err => -1, + std.log.Level.warn => 1, + std.log.Level.info => 3, + std.log.Level.debug => 4, + }; + if (opensips_level > c.log_level.*) return; + _ = c.dprint(opensips_level, c.log_facility, null, null, @ptrCast(@constCast(log)), @ptrCast(@constCast(log)), null); +} diff --git a/modules/crypto/src/main.zig b/modules/crypto/src/main.zig new file mode 100644 index 00000000000..348b8a5ca98 --- /dev/null +++ b/modules/crypto/src/main.zig @@ -0,0 +1,123 @@ +const std = @import("std"); +const opensips = @import("opensips.zig"); +const c = @cImport({ + @cInclude("sr_module.h"); +}); + +// Logger Initialization +pub const std_options = struct { + pub const log_level = .debug; + pub const logFn = @import("log.zig").logger; +}; + +const log = std.log.scoped(.crypto); + +// Module Definition +const module = opensips.moduleExport(.{ + .name = "crypto", + .cmds = &[_]c.cmd_export_t{ + opensips.cmdExport(.{ + .name = "rand", + .flags = c.ALL_ROUTES, + .function = &rand, + .param1_flags = c.CMD_PARAM_INT, + .param2_flags = c.CMD_PARAM_VAR, + }), + opensips.cmdExport(.{ + .name = "md5", + .flags = c.ALL_ROUTES, + .function = &md5, + .param1_flags = c.CMD_PARAM_STR, + .param2_flags = c.CMD_PARAM_VAR, + }), + opensips.cmdExport(.{ + .name = "sha1", + .flags = c.ALL_ROUTES, + .function = &sha1, + .param1_flags = c.CMD_PARAM_STR, + .param2_flags = c.CMD_PARAM_VAR, + }), + opensips.cmdExport(.{ + .name = "blake3", + .flags = c.ALL_ROUTES, + .function = &blake3, + .param1_flags = c.CMD_PARAM_STR, + .param2_flags = c.CMD_PARAM_VAR, + }), + }, + .init_f = &init, + .destroy_f = &deinit, +}); + +comptime { + @export(module, .{ .name = "exports" }); +} + +// Module Loaded Event +var rand_impl: std.rand.DefaultCsprng = undefined; + +fn init() callconv(.C) c_int { + var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; + _ = std.os.getrandom(&seed) catch { + log.err("The Crypto module load failed: failed to get initial random.", .{}); + return -1; + }; + rand_impl = std.rand.DefaultCsprng.init(seed); + std.crypto.utils.secureZero(u8, &seed); + + log.debug("The Crypto module has been loaded.", .{}); + return 0; +} + +// Module Unloaded Event +fn deinit() callconv(.C) c_int { + rand_impl = undefined; + log.debug("The Crypto module has been unloaded.", .{}); + return 0; +} + +// Exported Functions +pub fn rand(msg: [*c]const c.sip_msg, input_int: *c_int, out_var: *c.pv_spec_t) callconv(.C) c_int { + const len = opensips.inputInt(input_int); + var rand_raw: [1024]u8 = undefined; + if (len > rand_raw.len) { + std.log.err("Requested random bytes: {d}; upper limit: {d}. Failed to generate.", .{ len, rand_raw.len }); + return -1; + } + + rand_impl.random().bytes(rand_raw[0..len]); + var rand_hex: [rand_raw.len * 2]u8 = undefined; + var rand_hex_trimmed = std.fmt.bufPrint(&rand_hex, "{}", .{std.fmt.fmtSliceHexLower(rand_raw[0..len])}) catch unreachable; + + opensips.outStr(msg, out_var, rand_hex_trimmed) catch { + std.log.err("Output variable setting failed.", .{}); + return -1; + }; + return 1; +} + +pub fn md5(msg: [*c]const c.sip_msg, input_str: *c.__str, out_var: *c.pv_spec_t) callconv(.C) c_int { + return hash(std.crypto.hash.Md5, msg, input_str, out_var); +} + +pub fn sha1(msg: [*c]const c.sip_msg, input_str: *c.__str, out_var: *c.pv_spec_t) callconv(.C) c_int { + return hash(std.crypto.hash.Sha1, msg, input_str, out_var); +} + +pub fn blake3(msg: [*c]const c.sip_msg, input_str: *c.__str, out_var: *c.pv_spec_t) callconv(.C) c_int { + return hash(std.crypto.hash.Blake3, msg, input_str, out_var); +} + +pub fn hash(comptime T: type, msg: [*c]const c.sip_msg, input_str: *c.__str, out_var: *c.pv_spec_t) c_int { + const input = opensips.inputStr(input_str); + + var hash_raw: [T.digest_length]u8 = undefined; + T.hash(input, &hash_raw, .{}); + const hash_hex = std.fmt.bytesToHex(&hash_raw, .lower); + + opensips.outStr(msg, out_var, &hash_hex) catch { + std.log.err("Output variable setting failed.", .{}); + return -1; + }; + return 1; +} diff --git a/modules/crypto/src/opensips.zig b/modules/crypto/src/opensips.zig new file mode 100644 index 00000000000..8f1747cd4c6 --- /dev/null +++ b/modules/crypto/src/opensips.zig @@ -0,0 +1,84 @@ +const std = @import("std"); +pub const c = @cImport({ + @cInclude("sr_module.h"); +}); + +pub const OpenSIPSError = error{OpenSIPSError}; + +fn defaultInit(comptime T: type, init: anytype) T { + var data = std.mem.zeroes(T); + for (@typeInfo(@TypeOf(init)).Struct.fields) |field| { + if (@hasField(T, field.name)) { + const value = @field(init, field.name); + switch (@typeInfo(field.type)) { + .Pointer => { + @field(data, field.name) = @ptrCast(value); + }, + else => { + @field(data, field.name) = value; + }, + } + } + } + return data; +} + +pub fn moduleExport(init: anytype) c.module_exports { + var module = defaultInit(c.module_exports, init); + + // zig fmt: off + module.ver_info = .{ + .version = c.OPENSIPS_FULL_VERSION, + .compile_flags = c.OPENSIPS_COMPILE_FLAGS, + .scm = .{ + .type = c.VERSIONTYPE, + .rev = c.THISREVISION, + } + }; + // zig fmt: on + + return module; +} + +pub fn cmdExport(init: anytype) c.cmd_export_t { + const T = @TypeOf(init); + + var cmd = defaultInit(c.cmd_export_t, init); + + var params: [9]c.cmd_param = undefined; + for (0..params.len) |idx| { + const flags_field = std.fmt.comptimePrint("param{d}_flags", .{idx + 1}); + const fixup_field = std.fmt.comptimePrint("param{d}_fixup", .{idx + 1}); + const free_fixup_field = std.fmt.comptimePrint("param{d}_free_fixup_field", .{idx + 1}); + params[idx] = c.cmd_param{ + .flags = if (@hasField(T, flags_field)) @field(init, flags_field) else 0, + .fixup = if (@hasField(T, fixup_field)) @field(init, fixup_field) else null, + .free_fixup = if (@hasField(T, free_fixup_field)) @field(init, free_fixup_field) else null, + }; + } + cmd.params = params; + + return cmd; +} + +pub fn inputStr(input_str: *c.__str) []u8 { + return input_str.s[0..@intCast(input_str.len)]; +} + +pub fn inputInt(input_int: *c_int) usize { + return @intCast(input_int.*); +} + +pub fn outStr(msg: [*c]const c.sip_msg, out_var: *c.pv_spec_t, str: []const u8) OpenSIPSError!void { + const out_val = c.pv_value_t{ + .rs = .{ + .s = @ptrCast(@constCast(str)), + .len = @intCast(str.len), + }, + .ri = 0, + .flags = c.PV_VAL_STR, + }; + if (c.pv_set_value(@constCast(msg), out_var, 0, @constCast(&out_val)) != 0) { + return error.OpenSIPSError; + } +} diff --git a/scripts/build/do_build.sh b/scripts/build/do_build.sh index 08781da45f1..862de148ef0 100755 --- a/scripts/build/do_build.sh +++ b/scripts/build/do_build.sh @@ -7,7 +7,7 @@ set -e . $(dirname $0)/build.conf.sub EXCLUDE_MODULES="db_oracle osp sngtc cachedb_cassandra cachedb_couchbase \ - cachedb_mongodb auth_jwt event_kafka aaa_diameter" + cachedb_mongodb auth_jwt event_kafka aaa_diameter crypto" if [ ! -z "${EXCLUDE_MODULES_ADD}" ] then EXCLUDE_MODULES="${EXCLUDE_MODULES} ${EXCLUDE_MODULES_ADD}" diff --git a/test.cfg b/test.cfg new file mode 100644 index 00000000000..d658e715698 --- /dev/null +++ b/test.cfg @@ -0,0 +1,22 @@ +loadmodule "modules/crypto/crypto.so" +loadmodule "proto_udp.so" + +socket = udp:0.0.0.0:5060 + +startup_route { + $var(result) = ""; + $var(input) = "hello"; + + md5("$var(input)", $var(result)); + xlog("MD5 hash of \"$var(input)\": $var(result)\n"); + + sha1("$var(input)", $var(result)); + xlog("SHA1 hash of \"$var(input)\": $var(result)\n"); + + blake3("$var(input)", $var(result)); + xlog("Blake3 hash of \"$var(input)\": $var(result)\n"); + + $var(length) = 32; + rand($var(length), $var(result)); + xlog(" $var(length) random bytes: $var(result)\n"); +}