Skip to content

Commit

Permalink
Add a basically-functioning zig port
Browse files Browse the repository at this point in the history
* Approximately mirrors the C version's code, but zigified
* Passes existing tests used by the C version
* Seems to work correctly in the real world
* Supports zig versions 0.11.0 (latest) and 0.12.0-dev (master)
  • Loading branch information
blblack committed Feb 27, 2024
1 parent 13f1ce9 commit e4d36e5
Show file tree
Hide file tree
Showing 5 changed files with 802 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ rundir.inc
tofurkey
t/testout
t/testout_slow
zig-cache
zig-out
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,63 @@ Run slower tests with `make check SLOW_TESTS=1` (requires python3, and the "nacl
Run code quality checks with: `make qa` -- Note this requires several more tools (valgrind, cppcheck, clang analyzer, astyle) and may only work with the local versions of these that I happen to use! It's mostly for use during my development.

There's also a trivial example systemd unit file in the source tree as `tofurkey.service`

## Experimental Zig Port!

This repo now also contains an experimental port of tofurkey's C code to
zig. I made this port mostly as a learning exercise. I won't promise
anything about the quality of the ported code, as I'm still learning the
language and the language itself is not yet stable. Consider it
EXPERIMENTAL!

This port aims to mirror the C code as closely as it reasonably can,
making comparison of the two languages easy, while still taking
advantage of Zig stuff everywhere it can. It generally has the same
core function names, same approximate file layout, mostly the same
commentary and log outputs, etc. I plan to keep this port in sync with
changes to the C source as I go. As with the C code, it requires
libsodium (and headers).

Currently, this builds correctly with both zig 0.11.0 and zig's current
(as of this writing) master branch (0.12.0-dev), by the magic of
@hasDecl and @typeInfo.

### Building/testing/installing the experimental zig port

# Run unit tests (Zig-only):
zig build test
# Run quick integration tests (same tests as C "make check"):
zig build itest
# Run slow integration tests (same tests as C "make check SLOW_TESTS=1"):
zig build itest-slow
# Build + install output in-tree (creates ./zig-out/bin/tofurkey):
zig build install

# Notes:
# * All "zig build" commands can take optional arguments:
# -Doptimize={Debug|ReleaseSafe|ReleaseSmall|ReleaseFast}
# -Drundir=/run
# * For install step (the default), default installation prefix is
# ./zig-out, but you can override with e.g.:
# --prefix /usr
# * You can do more than one build command in one invocation
# * Results (executables, test results, etc) are cached locally and
# re-used by future invocations, assuming same -D args.
# * If you don't see an error message, assume it did the things
# successfully (zig tends to only generate output by default if
# something fails)
# * You can add flags "--summary all" and/or "--verbose" to see
# more of what's happening, including those silent successes
# and/or re-use of cached results.

# All-in-one: ReleaseSafe build mode, run unit tests, run quick
# integration tests, and install to ./zig-out :
zig build test itest install -Doptimize=ReleaseSafe

# Do the final install to system dirs (reuses cached compile from above):
sudo zig build install -Doptimize=ReleaseSafe --prefix /usr

### Known eventual TODOs for the Zig port:

* clock\_nanosleep() - I made a wrapper that's tailored to our use, but should build a generic real implementation to upstream to std.posix. It clearly belongs, it's just missing there currently.
* getopt() - I made a zig-style iterator interface to wrap libc for now, but really the whole getopt logic should just be implemented in native zig with a goal of upstreaming to std somewhere (not because getopt is all that awesome, more because it would ease adoption/ports for others with getopt()-based CLI parsing in old C projects).
71 changes: 71 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
// Standard options
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

// Define the executable.
// Later after dropping 0.11.0 support, can fold this up the "normal" way
// with addExecutable(.{ ... }))
var exe_opts = std.Build.ExecutableOptions{
.name = "tofurkey",
.root_source_file = .{ .path = "src/tofurkey.zig" },
.single_threaded = true,
.target = target,
.optimize = optimize,
};
if (@hasField(std.Build.ExecutableOptions, "strip")) // Not present in 0.11.0
exe_opts.strip = (optimize != .Debug);
const exe = b.addExecutable(exe_opts);

// Support for -Drundir=x affecting executable via config import
const rundir = b.option([]const u8, "rundir", "The system rundir, default '/run', for autokey storage") orelse "/run";
const options = b.addOptions();
options.addOption([]const u8, "rundir", rundir);
if (@hasDecl(@TypeOf(exe.*), "addOptions")) {
exe.addOptions("config", options); // 0.11.0
} else {
exe.root_module.addOptions("config", options); // master 0.12.0-dev
}

// Link libsodium and libc for the executable
exe.linkSystemLibrary("sodium");
exe.linkLibC();

// Declare the built executable as installable and put it in an overrideable sbindir
const sbindir = b.option([]const u8, "sbindir", "Prefix-relative subpath for sbin dir, default 'sbin'") orelse "sbin";
const exe_art = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = sbindir } } });
b.getInstallStep().dependOn(&exe_art.step);

// Install the manpage as well, with support for overriding the
// prefix-relative destination directory:
const man8dir = b.option([]const u8, "man8dir", "Prefix-relative subpath for man8 dir, default 'share/man/man8'") orelse "share/man/man8";
const man_page = b.addInstallFileWithDir(.{ .path = "tofurkey.8" }, .{ .custom = man8dir }, "tofurkey.8");
b.getInstallStep().dependOn(&man_page.step);

// "zig build test" -> Unit testing
const unit_exe = b.addTest(.{
.root_source_file = .{ .path = "src/tofurkey.zig" },
.single_threaded = true,
.target = target,
.optimize = optimize,
});
unit_exe.linkSystemLibrary("sodium");
unit_exe.linkLibC();
const run_unit_tests = b.addRunArtifact(unit_exe);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);

// "zig build itest" -> Quick integration testing with t/quick.sh
const itest_step = b.step("itest", "Run quick integration tests");
const itest_run_quick = b.addSystemCommand(&.{"t/quick.sh"});
itest_run_quick.addArtifactArg(exe);
itest_step.dependOn(&itest_run_quick.step);

// "zig build itest-slow" -> Full integration testing with t/quick.sh + t/slow.sh
const itest_step_slow = b.step("itest-slow", "Run full integration tests (slower)");
const itest_run_slow = b.addSystemCommand(&.{"t/slow.sh"});
itest_run_slow.addArtifactArg(exe);
itest_step_slow.dependOn(&itest_run_slow.step);
itest_step_slow.dependOn(&itest_run_quick.step);
}
154 changes: 154 additions & 0 deletions src/cwrappers.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// This file encapsulates all our direct uses of libc and libsodium into more
// zig-like interfaces for the main code. In theory, at least the libc parts
// could be obviated by improvements to the Zig Standard Library.

const std = @import("std");
const assert = std.debug.assert;
const log = std.log;
// Handle 0.11.0->0.12-dev switch from "os" to "posix"
const posix = if (@hasDecl(std, "posix")) std.posix else std.os;
const c = @cImport({
@cDefine("_GNU_SOURCE", {});
@cInclude("time.h"); // clock_nanosleep()
@cInclude("unistd.h"); // getopt()
@cInclude("sodium.h"); // libsodium
});

pub fn clock_nanosleep_real_abs(sec: u64, nsec: u64) !void {
// Assert that time_t (type of .tv_sec) can hold the same positive range as
// i64. This code is intentionally not compatible with 32-bit time_t!
comptime {
assert(std.math.maxInt(c.time_t) >= std.math.maxInt(i64));
}
// Assert the caller limits "sec" to not saturate i64 as well
assert(sec < std.math.maxInt(i64));
const ts = c.struct_timespec{ .tv_sec = @intCast(sec), .tv_nsec = @intCast(nsec) };
const rv = c.clock_nanosleep(c.CLOCK_REALTIME, c.TIMER_ABSTIME, &ts, null);
if (rv != 0)
return error.ClockNanosleepFailed;
}

test "clock_nanosleep sanity" {
// Get current time and sleep until then, with the nsec truncated to zero.
// Should return without any significant delay.
var ts = posix.timespec{ .tv_sec = 0, .tv_nsec = 0 };
try posix.clock_gettime(posix.CLOCK.REALTIME, &ts);
if (ts.tv_sec < 0)
return error.TimeRange;
try clock_nanosleep_real_abs(@intCast(ts.tv_sec), 0);
}

pub const GetOptIterator = struct {
c_argc: c_int,
c_argv: [*c]const [*c]u8,
c_optstr: [*:0]const u8,

pub fn init(argv: [][*:0]const u8, optstr: [:0]const u8) !GetOptIterator {
if (argv.len > std.math.maxInt(c_int))
return error.TooManyCLIArguments;
return .{
.c_argc = @intCast(argv.len),
.c_argv = @ptrCast(argv),
.c_optstr = optstr,
};
}

pub fn next(self: GetOptIterator) ?u8 {
const opt = c.getopt(self.c_argc, self.c_argv, self.c_optstr);
if (opt < 0)
return null;
return @truncate(@as(c_uint, @bitCast(opt)));
}

pub fn optarg(self: GetOptIterator) ?[*:0]const u8 {
_ = self;
return c.optarg;
}

pub fn optind(self: GetOptIterator) usize {
_ = self;
if (c.optind < 1) // JIC
return 1;
return @intCast(c.optind);
}

pub fn optopt(self: GetOptIterator) u8 {
_ = self;
return @bitCast(@as(i8, @truncate(c.optopt)));
}
};

//-----------------
// libsodium stuff
//-----------------

pub const b2b_CONTEXTBYTES = c.crypto_kdf_blake2b_CONTEXTBYTES;
pub const b2b_KEYBYTES = c.crypto_kdf_blake2b_KEYBYTES;
pub const b2b_BYTES_MIN = c.crypto_kdf_blake2b_BYTES_MIN;
pub const b2b_BYTES_MAX = c.crypto_kdf_blake2b_BYTES_MAX;

pub fn sodium_init() !void {
if (c.sodium_init() < 0)
return error.SodiumInitFailed;
}

pub fn sodium_memzero(mem: []u8) void {
c.sodium_memzero(@as(*anyopaque, @ptrCast(mem.ptr)), mem.len);
}

pub fn sodium_rand(mem: []u8) void {
c.randombytes_buf(@as(*anyopaque, @ptrCast(mem.ptr)), mem.len);
}

pub fn b2b_derive_from_key(out: *[16]u8, len: usize, subkey: u64, ctx: *const [8]u8, key: *const [32]u8) !void {
const rv = c.crypto_kdf_blake2b_derive_from_key(out, len, subkey, ctx, key);
if (rv != 0)
return error.Blake2BFailed;
}

test "blake2b KDF alg check" {
var outbuf: [16]u8 = undefined;
const ctx = [_]u8{ 't', 'o', 'f', 'u', 'r', 'k', 'e', 'y' };
const key = [_]u8{
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
};
try b2b_derive_from_key(&outbuf, 16, 1234, &ctx, &key);
const expect_out = [_]u8{
0x0E, 0xB0, 0x0F, 0x64, 0x3E, 0xB0, 0x4E, 0x60,
0x9D, 0x5B, 0x23, 0x18, 0xEB, 0x67, 0x52, 0x31,
};
try std.testing.expectEqualSlices(u8, &expect_out, &outbuf);
}

// A zig allocator wrapping simple use of sodium_malloc/free
pub fn SodiumAllocator() type {
return struct {
pub fn allocator(self: *@This()) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = sodium_alloc,
.resize = std.mem.Allocator.noResize,
.free = sodium_free,
},
};
}

fn sodium_alloc(ctx: *anyopaque, len: usize, log2_ptr_align: u8, ret_addr: usize) ?[*]u8 {
_ = ctx;
_ = log2_ptr_align;
_ = ret_addr;
return @as(?[*]u8, @ptrCast(c.sodium_malloc(len)));
}

fn sodium_free(ctx: *anyopaque, old_mem: []u8, log2_old_align_u8: u8, ret_addr: usize) void {
_ = ctx;
_ = log2_old_align_u8;
_ = ret_addr;
c.sodium_free(@as(*anyopaque, @ptrCast(old_mem)));
}
};
}
Loading

0 comments on commit e4d36e5

Please sign in to comment.