f6be0329e2a7 — Nathan Michaels 5 years ago
Add crypto_sign for signatures and verification.

Also refactored errors: they've been consolidated.
6 files changed, 139 insertions(+), 26 deletions(-)

M src/crypto_box.zig
A => src/crypto_sign.zig
A => src/errors.zig
M src/randombytes.zig
M src/secretstream.zig
M src/sodium.zig
M src/crypto_box.zig +7 -8
@@ 2,6 2,8 @@ const std = @import("std");
 const testing = std.testing;
 const Allocator = std.mem.Allocator;
 const randombytes = @import("randombytes.zig");
+const sodium = @import("sodium.zig");
+const SodiumError = sodium.SodiumError;
 
 const c = @cImport({
     @cInclude("sodium.h");

          
@@ 28,14 30,6 @@ pub const MACBYTES = c.crypto_box_MACBYT
 //
 // Wasn't that enlightening?
 // TODO: Figure out what kinds of errors these things can return.
-const SodiumError = error{
-    KeyGenError,
-    SealError,
-    UnsealError,
-    EncryptError,
-    OpenError,
-    BufferTooSmall,
-};
 
 /// Generate a public/private key pair for use in other functions in
 /// this module.

          
@@ 147,6 141,8 @@ pub fn open_easy(
     }
 }
 
+// TODO: wrap sodium_malloc in an Allocator and point people to it.
+
 /// Dynamic sealing object. Uses a mem.Allocator to allocate memory
 /// for ciphertext.
 ///

          
@@ 219,6 215,7 @@ pub const Unsealer = struct {
 };
 
 test "seal" {
+    try sodium.init();
     var pk: [PUBLICKEYBYTES]u8 = undefined;
     var sk: [SECRETKEYBYTES]u8 = undefined;
 

          
@@ 235,6 232,7 @@ test "seal" {
 }
 
 test "sealer" {
+    try sodium.init();
     var pk: [PUBLICKEYBYTES]u8 = undefined;
     var sk: [SECRETKEYBYTES]u8 = undefined;
     try keyPair(&pk, &sk);

          
@@ 253,6 251,7 @@ test "sealer" {
 }
 
 test "authenticated encryption" {
+    try sodium.init();
     var alice_pub: [PUBLICKEYBYTES]u8 = undefined;
     var alice_priv: [SECRETKEYBYTES]u8 = undefined;
     var bob_pub: [PUBLICKEYBYTES]u8 = undefined;

          
A => src/crypto_sign.zig +93 -0
@@ 0,0 1,93 @@ 
+const std = @import("std");
+const testing = std.testing;
+const sodium = @import("sodium.zig");
+const SodiumError = sodium.SodiumError;
+
+const c = @cImport({
+    @cInclude("sodium.h");
+});
+
+const PUBLICKEYBYTES = c.crypto_sign_PUBLICKEYBYTES;
+const SECRETKEYBYTES = c.crypto_sign_SECRETKEYBYTES;
+const BYTES = c.crypto_sign_BYTES;
+
+/// Generate a key pair for signing. Don't share the secret half.
+pub fn keyPair(
+    pub_key: *[PUBLICKEYBYTES]u8,
+    secret_key: *[SECRETKEYBYTES]u8,
+) !void {
+    if (c.crypto_sign_keypair(pub_key, secret_key) != 0) {
+        return SodiumError.KeyGenError;
+    }
+}
+
+/// Sign a message with secret_key. Puts signed result in signed_msg,
+/// which must be at least BYTES larger than msg.
+pub fn sign(
+    signed_msg: []u8,
+    message: []const u8,
+    secret_key: *[SECRETKEYBYTES]u8,
+) !void {
+    if (signed_msg.len < message.len + BYTES) {
+        return SodiumError.BufferTooSmall;
+    }
+
+    var len: c_ulonglong = undefined;
+    if (c.crypto_sign(
+        signed_msg.ptr,
+        &len,
+        message.ptr,
+        message.len,
+        secret_key,
+    ) != 0) {
+        return SodiumError.SignError;
+    }
+
+    if (len > signed_msg.len) {
+        return SodiumError.BufferTooSmall;
+    }
+}
+
+/// Verify the signature on signed_msg, and put the raw message in
+/// msg, which must be able to hold at least (signed_msg.len - BYTES)
+/// bytes. Use the signer's public key.
+pub fn open(
+    msg: []u8,
+    signed_msg: []const u8,
+    public_key: *[PUBLICKEYBYTES]u8,
+) !void {
+    if (msg.len + BYTES < signed_msg.len) {
+        return SodiumError.BufferTooSmall;
+    }
+
+    var len: c_ulonglong = undefined;
+    if (c.crypto_sign_open(
+        msg.ptr,
+        &len,
+        signed_msg.ptr,
+        signed_msg.len,
+        public_key,
+    ) != 0) {
+        // It's an error because your program should crash if it
+        // doesn't handle it.
+        return SodiumError.InvalidSignature;
+    }
+    if (msg.len < len) {
+        return SodiumError.BufferTooSmall;
+    }
+}
+
+test "signature" {
+    try sodium.init();
+    var pub_key: [PUBLICKEYBYTES]u8 = undefined;
+    var secret_key: [SECRETKEYBYTES]u8 = undefined;
+
+    try keyPair(&pub_key, &secret_key);
+
+    const msg = "A special message that should not be changed."[0..];
+    var signed: [msg.len + BYTES]u8 = undefined;
+    try sign(signed[0..], msg, &secret_key);
+    var received: [signed.len - BYTES]u8 = undefined;
+    try open(received[0..], signed[0..], &pub_key);
+    testing.expectEqualSlices(u8, msg, received[0..]);
+}

          
A => src/errors.zig +14 -0
@@ 0,0 1,14 @@ 
+pub const SodiumError = error{
+    KeyGenError,
+    SealError,
+    UnsealError,
+    EncryptError,
+    OpenError,
+    BufferTooSmall,
+    ChunkTooBig,
+    InvalidCiphertext,
+    InitError,
+    InvalidHeader,
+    SignError,
+    InvalidSignature,
+};

          
M src/randombytes.zig +2 -0
@@ 1,5 1,6 @@ 
 const std = @import("std");
 const testing = std.testing;
+const sodium = @import("sodium.zig");
 
 const c = @cImport({
     @cInclude("sodium.h");

          
@@ 47,6 48,7 @@ pub fn stir(void) void {
 }
 
 test "random numbers" {
+    try sodium.init();
     _ = random();
     _ = uniform(0xd00dface);
     var buffer = [_]u8{ 1, 2, 3, 4, 5 };

          
M src/secretstream.zig +13 -18
@@ 2,20 2,13 @@ const std = @import("std");
 const Allocator = std.mem.Allocator;
 const ArrayList = std.ArrayList;
 const testing = std.testing;
+const sodium = @import("sodium.zig");
+const SodiumError = sodium.SodiumError;
 
 const c = @cImport({
     @cInclude("sodium.h");
 });
 
-const StreamError = error{
-    BufferTooShort,
-    ChunkTooBig,
-    EncryptError,
-    InvalidCiphertext,
-    InitError,
-    InvalidHeader,
-};
-
 const Tag = enum {
     MESSAGE = c.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE,
     PUSH = c.crypto_secretstream_xchacha20poly1305_TAG_PUSH,

          
@@ 44,7 37,7 @@ pub fn init_push(
 ) !void {
     const fun = c.crypto_secretstream_xchacha20poly1305_init_push;
     if (fun(state, header, key) != 0) {
-        return StreamError.InitError;
+        return SodiumError.InitError;
     }
 }
 

          
@@ 62,7 55,7 @@ pub fn push(
     // crypto_secretstream_xchacha20poly1305_ABYTES, so let's make
     // sure there's room.
     if (ciphertext.len < (message.len + ABYTES)) {
-        return StreamError.BufferTooShort;
+        return SodiumError.BufferTooSmall;
     }
     const res = c.crypto_secretstream_xchacha20poly1305_push(
         state,

          
@@ 76,10 69,10 @@ pub fn push(
     );
     if (clen > ciphertext.len) {
         // I don't trust guarantees that aren't in the source code.
-        return StreamError.BufferTooShort;
+        return SodiumError.BufferTooSmall;
     }
     if (res != 0) {
-        return StreamError.EncryptError;
+        return SodiumError.EncryptError;
     }
 }
 

          
@@ 92,7 85,7 @@ pub fn init_pull(
 ) !void {
     const fun = c.crypto_secretstream_xchacha20poly1305_init_pull;
     if (fun(state, header, key) != 0) {
-        return StreamError.InvalidHeader;
+        return SodiumError.InvalidHeader;
     }
 }
 

          
@@ 106,7 99,7 @@ pub fn pull(
     additional_data: ?[]const u8,
 ) !void {
     if (message.len < ciphertext.len - ABYTES) {
-        return StreamError.BufferTooShort;
+        return SodiumError.BufferTooSmall;
     }
     var mlen: c_ulonglong = undefined;
     const res = c.crypto_secretstream_xchacha20poly1305_pull(

          
@@ 120,10 113,10 @@ pub fn pull(
         if (additional_data) |ad| ad.len else 0,
     );
     if (res != 0) {
-        return StreamError.InvalidCiphertext;
+        return SodiumError.InvalidCiphertext;
     }
     if (mlen > message.len) {
-        return StreamError.BufferTooShort;
+        return SodiumError.BufferTooSmall;
     }
 }
 

          
@@ 161,7 154,7 @@ pub fn ChunkEncrypter(chunk_size: usize)
         /// Call with data to encrypt it.
         pub fn push_chunk(self: *Self, msg: []const u8) !void {
             if (msg.len > chunk_size) {
-                return StreamError.ChunkTooBig;
+                return SodiumError.ChunkTooBig;
             }
             // There's probably a better way to do this than to copy
             // the whole array over to the stack, byte by byte.

          
@@ 229,6 222,7 @@ pub fn ChunkDecrypter(chunk_size: usize)
 }
 
 test "stream" {
+    try sodium.init();
     const msg = "Check check wheeee";
     const msg2 = "Eekers";
     var key: [KEYBYTES]u8 = undefined;

          
@@ 252,6 246,7 @@ test "stream" {
 }
 
 test "chunks" {
+    try sodium.init();
     const msg = "This message is longer than my chunk size.";
     const malloc = std.heap.c_allocator;
     var key: [KEYBYTES]u8 = undefined;

          
M src/sodium.zig +10 -0
@@ 4,10 4,20 @@ const c = @cImport({
 });
 
 pub const crypto_box = @import("crypto_box.zig");
+pub const crypto_sign = @import("crypto_sign.zig");
 pub const secretstream = @import("secretstream.zig");
+pub const randombytes = @import("randombytes.zig");
+pub const SodiumError = @import("errors.zig").SodiumError;
+
+pub fn init() !void {
+    if (c.sodium_init() < 0) {
+        return SodiumError.InitError;
+    }
+}
 
 test "nacl" {
     _ = @import("crypto_box.zig");
     _ = @import("secretstream.zig");
     _ = @import("randombytes.zig");
+    _ = @import("crypto_sign.zig");
 }