From bd9ffc828ee34bbf9fc887ceefff5b88a3b81a17 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Fri, 5 Jun 2015 15:19:58 +0200 Subject: [PATCH] Initial import --- .gitignore | 27 +++ CMakeLists.txt | 14 ++ LICENSE | 16 ++ README.md | 122 +++++++++++ src/base64.c | 113 ++++++++++ src/base64.h | 17 ++ src/get_line.c | 88 ++++++++ src/get_line.h | 8 + src/helpers.c | 184 ++++++++++++++++ src/helpers.h | 34 +++ src/minisign.c | 575 +++++++++++++++++++++++++++++++++++++++++++++++++ src/minisign.h | 58 +++++ 12 files changed, 1256 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/base64.c create mode 100644 src/base64.h create mode 100644 src/get_line.c create mode 100644 src/get_line.h create mode 100644 src/helpers.c create mode 100644 src/helpers.h create mode 100644 src/minisign.c create mode 100644 src/minisign.h diff --git a/.gitignore b/.gitignore index e69de29..c1b24bd 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,27 @@ +*.cmake +*.dSYM +*.exp +*.gcda +*.gcno +*.la +*.lo +*.log +*.mem +*.o +*.plist +*.scan +*.sdf +*.status +*.tar.* +*~ +.DS_Store +.deps +.dirstamp +.done +.libs +CMakeCache.txt +CMakeFiles +Makefile +cmake_install.cmake +minisign + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a9fc31a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 2.8) + +project(minisign C) + +include(CheckLibraryExists) + +find_library(LIB_SODIUM NAMES sodium REQUIRED) + +add_executable(minisign + src/minisign.c src/base64.c src/helpers.c src/get_line.c) + +target_link_libraries(minisign ${LIB_SODIUM}) + +install(TARGETS minisign DESTINATION bin) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1ccc2e6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2015 + * Frank Denis + * + * 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. + */ diff --git a/README.md b/README.md new file mode 100644 index 0000000..60e6bcc --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ + +Minisign +======== + +Minisign is a dead simple tool to sign files and verify signatures. + +Compilation / installation +-------------------------- + +Dependencies: +* [libsodium](http://doc.libsodium.org/) +* cmake + +Compilation: + + $ cmake . + $ make + # make install + +Creating a key pair +------------------- + + $ minisign -G + +The public key is put into the `minisign.pub` file, and the secret key +into the `minisign.key` file. + +Signing a file +-------------- + + $ minisign -S -m myfile.txt + +Or to include a comment in the signature, that will be verified and +displayed when verifying the file: + + $ minisign -S -m myfile.txt -t 'This comment will be signed as well' + +The signature is put into `myfile.txt.minisig`. + +Verifying a file +---------------- + + $ minisign -V -m myfile.txt + +This requires the signature `myfile.txt.minisig` to be present in the same +directory. + +Options list +------------ + + $ minisign -G -p pubkey -s seckey + + $ minisign -S -s seckey -m file [-x sigfile] [-c untrusted_comment] [-t trusted_comment] + + $ minisign -V -p pubkey -m file [-x sigfile] [-q] + +Trusted comments +---------------- + +Signature files include an untrusted comment line that can be freely +modified, even after signature creation. + +They also include a second comment line, that cannot be modified +without the secret key. + +Trusted comments can be used to add instructions or application-specific +metadata (timestamps, version numbers, resource identifiers). + +Compatibility with OpenBSD signify +---------------------------------- + +Signature written by minisign can be verified using OpenBSD's signify +tool: public key files and signature files are compatible. + +However, minisign uses a slightly different format to store private keys. + +minisign signatures include trusted comments in addition to untrusted +comments. Trusted comments are signed, thus verified, before being +displayed. +This adds two lines to the signature files, that signify silently ignores. +minisign has to be used in order to verify trusted comments. + +Signature format +---------------- + + untrusted comment: + base64( || || ) + trusted_comment: + base64() + +* `signature_algorithm`: `Ed` +* `key_id`: 8 random bytes, matching the public key +* `signature`: ed25519() +* `global_signature`: ed25519( || ) + +Public key format +----------------- + + untrusted comment: + base64( || || ) + +* `signature_algorithm`: `Ed` +* `key_id`: 8 random bytes +* `public_key`: Ed25519 public key + +Secret key format +----------------- + + untrusted comment: + base64( || || || + || || || ) + +* `signature_algorithm`: `Ed` +* `kdf_algorithm`: `Sc` +* `cksum_algorithm`: `B2` +* `kdf_salt`: 32 random bytes +* `kdf_opslimit`: `crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_SENSITIVE` +* `kdf_memlimit`: `crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_SENSITIVE` +* `keynum_sk`: ` ^ ( || secret_key> || )` +* `key_id`: 8 random bytes +* `secret_key`: Ed25519 secret key +* `checksum`: `Blake2b( || )`, 32 bytes diff --git a/src/base64.c b/src/base64.c new file mode 100644 index 0000000..17e08b3 --- /dev/null +++ b/src/base64.c @@ -0,0 +1,113 @@ + +#include +#include + +#include "base64.h" + +unsigned char * +b64_to_bin(unsigned char * const bin, const char *b64, + size_t bin_maxlen, size_t b64_len, size_t * const bin_len_p) +{ +#define REV64_EOT 128U +#define REV64_NONE 64U +#define REV64_PAD '=' + + static const unsigned char rev64chars[256] = { + REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, + REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, + REV64_NONE, REV64_NONE, REV64_NONE, 62U, REV64_NONE, REV64_NONE, REV64_NONE, 63U, 52U, 53U, 54U, 55U, 56U, 57U, 58U, 59U, 60U, 61U, REV64_NONE, REV64_NONE, REV64_NONE, REV64_EOT, REV64_NONE, REV64_NONE, REV64_NONE, 0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, + 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U, 18U, 19U, 20U, 21U, 22U, 23U, 24U, 25U, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, 26U, 27U, 28U, 29U, 30U, 31U, 32U, 33U, 34U, 35U, 36U, 37U, 38U, 39U, 40U, 41U, 42U, + 43U, 44U, 45U, 46U, 47U, 48U, 49U, 50U, 51U, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, + REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, + REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, + REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, + REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, + REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, + REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE, REV64_NONE + }; + const unsigned char *b64_u = (const unsigned char *) b64; + unsigned char *bin_w = bin; + unsigned char mask; + unsigned char t0, t1, t2, t3; + uint32_t t; + size_t i; + + if (b64_len % 4U != 0U || (i = b64_len / 4U) <= 0U || bin_maxlen < i * 3U - + (b64_u[b64_len - 1U] == REV64_PAD) - (b64_u[b64_len - 2U] == REV64_PAD)) { + return NULL; + } + while (i-- > 0U) { + t0 = rev64chars[*b64++]; + t1 = rev64chars[*b64++]; + t2 = rev64chars[*b64++]; + t3 = rev64chars[*b64++]; + t = t3 | ((uint32_t) t2 << 6) | ((uint32_t) t1 << 12) | + ((uint32_t) t0 << 18); + mask = t0 | t1 | t2 | t3; + if ((mask & (REV64_NONE | REV64_EOT)) != 0U) { + if ((mask & REV64_NONE) != 0U || i > 0U) { + return NULL; + } + break; + } + *bin_w++ = (unsigned char) (t >> 16); + *bin_w++ = (unsigned char) (t >> 8); + *bin_w++ = (unsigned char) t; + } + if ((mask & REV64_EOT) != 0U) { + if (((t0 | t1) & REV64_EOT) != 0U || t3 != REV64_EOT) { + return NULL; + } + *bin_w++ = (unsigned char) (t >> 16); + if (t2 != REV64_EOT) { + *bin_w++ = (unsigned char) (t >> 8); + } + } + if (bin_len_p != NULL) { + *bin_len_p = (size_t) (bin_w - bin); + } + return bin; +} + +char *bin_to_b64(char * const b64, const unsigned char *bin, + size_t b64_maxlen, size_t bin_len) +{ +#define B64_PAD '=' + + static const char b64chars[64] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + char *b64_w = b64; + + if (b64_maxlen < (((bin_len + 2U) / 3U) * 4U + 1U)) { + return NULL; + } + while (bin_len > (size_t) 2U) { + const unsigned char t0 = (unsigned char) *bin++; + const unsigned char t1 = (unsigned char) *bin++; + const unsigned char t2 = (unsigned char) *bin++; + + *b64_w++ = b64chars[(t0 >> 2) & 63]; + *b64_w++ = b64chars[((t0 << 4) & 48) | ((t1 >> 4) & 15)]; + *b64_w++ = b64chars[((t1 << 2) & 60) | ((t2 >> 6) & 3)]; + *b64_w++ = b64chars[t2 & 63]; + bin_len -= (size_t) 3U; + } + if (bin_len > (size_t) 0U) { + const unsigned char t0 = (unsigned char) bin[0]; + + *b64_w++ = b64chars[(t0 >> 2) & 63]; + if (bin_len == 1U) { + *b64_w++ = b64chars[((t0 << 4) & 48)]; + *b64_w++ = B64_PAD; + } else { + const unsigned char t1 = (unsigned char) bin[1]; + + *b64_w++ = b64chars[((t0 << 4) & 48) | ((t1 >> 4) & 15)]; + *b64_w++ = b64chars[((t1 << 2) & 60)]; + } + *b64_w++ = B64_PAD; + } + *b64_w = 0; + + return b64; +} diff --git a/src/base64.h b/src/base64.h new file mode 100644 index 0000000..1bb704f --- /dev/null +++ b/src/base64.h @@ -0,0 +1,17 @@ + +#ifndef BASE64_H +#define BASE64_H + +#include + +unsigned char *b64_to_bin(unsigned char * const bin, const char *b64, + size_t bin_maxlen, size_t b64_len, + size_t * const bin_len_p); + +char *bin_to_b64(char * const b64, const unsigned char *bin, + size_t b64_maxlen, size_t bin_len); + +#define B64_MAX_LEN_FROM_BIN_LEN(X) (((X) + 2) / 3 * 4 + 1) +#define BIN_MAX_LEN_FROM_B64_LEN(X) ((X) / 4 * 3) + +#endif diff --git a/src/get_line.c b/src/get_line.c new file mode 100644 index 0000000..02fcbff --- /dev/null +++ b/src/get_line.c @@ -0,0 +1,88 @@ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "get_line.h" +#include "helpers.h" + +#ifndef TCSAFLUSH +# define TCSAFLUSH 0 +#endif + +#ifndef VERIFY_ONLY + +static void +disable_echo(void) +{ + struct termios p; + + fpurge(stdin); + fflush(stdout); + fflush(stderr); + if (!isatty(0) || tcgetattr(0, &p) != 0) { + return; + } + p.c_lflag &= ~ECHO; + tcsetattr(0, TCSAFLUSH, &p); +} + +static void +enable_echo(void) +{ + struct termios p; + + fpurge(stdin); + fflush(stdout); + fflush(stderr); + if (!isatty(0) || tcgetattr(0, &p) != 0) { + return; + } + p.c_lflag |= ECHO; + tcsetattr(0, TCSAFLUSH, &p); +} + +int +get_line(char *line, size_t max_len, const char *prompt) +{ + memset(line, 0, max_len); + if (max_len < 2U || max_len > INT_MAX) { + return -1; + } + xfprintf(stderr, "%s", prompt); + fflush(stderr); + if (fgets(line, (int) max_len, stdin) == NULL) { + return -1; + } + trim(line); + if (strlen(line) >= max_len) { + fprintf(stderr, "(truncated to %u characters)\n", (int) max_len); + } else if (*line == 0) { + fprintf(stderr, "(empty)\n"); + } else { + fprintf(stderr, "\n"); + } + return 0; +} + +int +get_password(char *pwd, size_t max_len, const char *prompt) +{ + int ret; + + disable_echo(); + ret = get_line(pwd, max_len, prompt); + enable_echo(); + + return ret; +} + +#endif diff --git a/src/get_line.h b/src/get_line.h new file mode 100644 index 0000000..0e2e777 --- /dev/null +++ b/src/get_line.h @@ -0,0 +1,8 @@ + +#ifndef GET_LINE_H +#define GET_LINE_H + +int get_line(char *line, size_t max_len, const char *prompt); +int get_password(char *pwd, size_t max_len, const char *prompt); + +#endif diff --git a/src/helpers.c b/src/helpers.c new file mode 100644 index 0000000..2036b2a --- /dev/null +++ b/src/helpers.c @@ -0,0 +1,184 @@ + +#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +# include +# include +# include +#endif + +#include +#include +#include +#include +#include + +#include + +#include "base64.h" +#include "helpers.h" + +uint64_t +le64_load(const unsigned char *p) +{ + return ((uint64_t)(p[0])) | ((uint64_t)(p[1]) << 8) | + ((uint64_t)(p[2]) << 16) | ((uint64_t)(p[3]) << 24) | + ((uint64_t)(p[4]) << 32) | ((uint64_t)(p[5]) << 40) | + ((uint64_t)(p[6]) << 48) | ((uint64_t)(p[7]) << 56); +} + +void +le64_store(unsigned char *p, uint64_t x) +{ + p[0] = (unsigned char) x; + p[1] = (unsigned char) (x >> 8); + p[2] = (unsigned char) (x >> 16); + p[3] = (unsigned char) (x >> 24); + p[4] = (unsigned char) (x >> 32); + p[5] = (unsigned char) (x >> 40); + p[6] = (unsigned char) (x >> 48); + p[7] = (unsigned char) (x >> 56); +} + +void +exit_err(const char *msg) +{ + perror(msg == NULL ? "" : msg); + exit(1); +} + +void +exit_msg(const char *msg) +{ + fprintf(stderr, "%s\n", msg); + exit(1); +} + +void * +xmalloc(size_t size) +{ + void *pnt; + + if ((pnt = malloc(size)) == NULL) { + exit_err("malloc()"); + } + return pnt; +} + +void * +xsodium_malloc(size_t size) +{ + void *pnt; + + if ((pnt = sodium_malloc(size)) == NULL) { + exit_err("sodium_malloc()"); + } + return pnt; +} + +void +xor_buf(unsigned char *dst, const unsigned char *src, size_t len) +{ + size_t i; + + for (i = (size_t) 0U; i < len; i++) { + dst[i] ^= src[i]; + } +} + +int +xfprintf(FILE *fp, const char *format, ...) +{ + char *out; + size_t out_maxlen = 4096U; + int len; + va_list va; + + va_start(va, format); + out = xsodium_malloc(out_maxlen); + len = vsnprintf(out, out_maxlen, format, va); + if (len < 0 || len >= (int) out_maxlen) { + va_end(va); + exit_msg("xfprintf() overflow"); + } + va_end(va); + if (fwrite(out, (size_t) len, 1U, fp) != 1U) { + sodium_free(out); + va_end(va); + exit_err("fwrite()"); + } + sodium_free(out); + + return 0; +} + +int +xfput_b64(FILE *fp, const unsigned char *bin, size_t bin_len) +{ + const size_t b64_maxlen = (bin_len + 2) * 4 / 3 + 1; + char *b64; + + b64 = xsodium_malloc(b64_maxlen); + if (bin_to_b64(b64, bin, b64_maxlen, bin_len) == NULL) { + sodium_free(b64); + abort(); + } + xfprintf(fp, "%s\n", b64); + sodium_free(b64); + + return 0; +} + +int +xfclose(FILE *fp) +{ + if (fp == NULL) { + abort(); + } + if (fclose(fp) != 0) { + exit_err("fclose()"); + } + return 0; +} + +void +trim(char *str) +{ + size_t i = strlen(str); + + while (i-- > (size_t) 0U) { + if (str[i] == '\n' || str[i] == '\r') { + str[i] = 0; + } + } +} + +const char * +file_basename(const char *file) +{ + char *ptr; + + if ((ptr = strrchr(file, '/')) != NULL) { + return ptr + 1; + } +#ifdef _WIN32 + if ((ptr = strrchr(file, '\\')) != NULL) { + return ptr + 1; + } +#endif + return file; +} + +FILE * +fopen_create_useronly(const char *file) +{ +#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) + int fd; + + if ((fd = open(file, O_CREAT | O_TRUNC | O_WRONLY, + (mode_t) 0600)) == -1) { + return NULL; + } + return fdopen(fd, "w"); +#else + return fopen(file, "w"); +#endif +} diff --git a/src/helpers.h b/src/helpers.h new file mode 100644 index 0000000..bdd4c3f --- /dev/null +++ b/src/helpers.h @@ -0,0 +1,34 @@ + +#ifndef HELPERS_H +#define HELPERS_H 1 + +#include +#include + +uint64_t le64_load(const unsigned char *p); + +void le64_store(unsigned char *p, uint64_t x); + +void exit_err(const char *msg) __attribute__((noreturn)); + +void exit_msg(const char *msg) __attribute__((noreturn)); + +void * xmalloc(size_t size); + +void * xsodium_malloc(size_t size); + +void xor_buf(unsigned char *dst, const unsigned char *src, size_t len); + +int xfput_b64(FILE *fp, const unsigned char *bin, size_t bin_len); + +int xfprintf(FILE *fp, const char *format, ...) __attribute__((format(printf, 2, 3))); + +int xfclose(FILE *fp); + +void trim(char *str); + +const char * file_basename(const char *file); + +FILE * fopen_create_useronly(const char *file); + +#endif diff --git a/src/minisign.c b/src/minisign.c new file mode 100644 index 0000000..410d318 --- /dev/null +++ b/src/minisign.c @@ -0,0 +1,575 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "base64.h" +#include "get_line.h" +#include "helpers.h" +#include "minisign.h" + +#ifndef VERIFY_ONLY +static const char *getopt_options = "GSVhc:m:p:qs:t:x:"; +#else +static const char *getopt_options = "Vhm:p:qx:"; +#endif + +static void usage(void) __attribute__((noreturn)); + +static void +usage(void) +{ + puts("Usage:\n" +#ifndef VERIFY_ONLY + "minisign -G -p pubkey -s seckey\n" + "minisign -S -s seckey -m file [-x sigfile] [-c untrusted_comment] [-t trusted_comment]\n" +#endif + "minisign -V -p pubkey -m file [-x sigfile] [-q]\n"); + exit(1); +} + +static unsigned char * +message_load(size_t *message_len, const char *message_file) +{ + FILE *fp; + unsigned char *message; + off_t message_len_; + + if ((fp = fopen(message_file, "r")) == NULL || + fseeko(fp, 0 , SEEK_END) != 0 || + (message_len_ = ftello(fp)) == (off_t) -1) { + exit_err(message_file); + } + if (message_len_ > (off_t) 1L << 30) { + exit_msg("Data has to be smaller than 1 Gb"); + } + if ((uintmax_t) message_len_ > (uintmax_t) SIZE_MAX || + message_len_ < (off_t) 0) { + abort(); + } + message = xmalloc((*message_len = (size_t) message_len_)); + rewind(fp); + if (fread(message, *message_len, (size_t) 1U, fp) != 1U) { + exit_err(message_file); + } + xfclose(fp); + + return message; +} + +static SigStruct * +sig_load(const char *sig_file, unsigned char global_sig[crypto_sign_BYTES], + char trusted_comment[TRUSTEDCOMMENTMAXBYTES], + size_t trusted_comment_maxlen) +{ + char comment[COMMENTMAXBYTES]; + SigStruct *sig_struct; + FILE *fp; + char *global_sig_s; + char *sig_s; + size_t global_sig_len; + size_t global_sig_s_size; + size_t sig_s_size; + size_t sig_struct_len; + + if ((fp = fopen(sig_file, "r")) == NULL) { + exit_err(sig_file); + } + if (fgets(comment, (int) sizeof comment, fp) == NULL) { + exit_err(sig_file); + } + if (strncmp(comment, COMMENT_PREFIX, (sizeof COMMENT_PREFIX) - 1U) != 0) { + exit_msg("Untrusted signature comment should start with " + "\"" COMMENT_PREFIX "\""); + } + sig_s_size = B64_MAX_LEN_FROM_BIN_LEN(sizeof *sig_struct) + 1U; + sig_s = xmalloc(sig_s_size); + if (fgets(sig_s, (int) sig_s_size, fp) == NULL) { + exit_err(sig_file); + } + trim(sig_s); + if (fgets(trusted_comment, (int) trusted_comment_maxlen, fp) == NULL) { + exit_err(sig_file); + } + if (strncmp(trusted_comment, TRUSTED_COMMENT_PREFIX, + (sizeof TRUSTED_COMMENT_PREFIX) - 1U) != 0) { + exit_msg("Trusted signature comment should start with " + "\"" TRUSTED_COMMENT_PREFIX "\""); + } + memmove(trusted_comment, + trusted_comment + sizeof TRUSTED_COMMENT_PREFIX - 1U, + strlen(trusted_comment + sizeof TRUSTED_COMMENT_PREFIX - 1U) + 1U); + trim(trusted_comment); + global_sig_s_size = B64_MAX_LEN_FROM_BIN_LEN(crypto_sign_BYTES) + 1U; + global_sig_s = xmalloc(global_sig_s_size); + if (fgets(global_sig_s, (int) global_sig_s_size, fp) == NULL) { + exit_err(sig_file); + } + trim(global_sig_s); + xfclose(fp); + + sig_struct = xmalloc(sizeof *sig_struct); + if (b64_to_bin((unsigned char *) (void *) sig_struct, sig_s, + sizeof *sig_struct, strlen(sig_s), + &sig_struct_len) == NULL || + sig_struct_len != sizeof *sig_struct) { + exit_msg("base64 conversion failed"); + } + free(sig_s); + if (memcmp(sig_struct->sig_alg, SIGALG, sizeof sig_struct->sig_alg) != 0) { + exit_msg("Unsupported signature algorithm"); + } + if (b64_to_bin(global_sig, global_sig_s, crypto_sign_BYTES, + strlen(global_sig_s), &global_sig_len) == NULL || + global_sig_len != crypto_sign_BYTES) { + exit_msg("base64 conversion failed"); + } + free(global_sig_s); + + return sig_struct; +} + +static PubkeyStruct * +pubkey_load(const char *pk_file) +{ + char pk_comment[COMMENTMAXBYTES]; + PubkeyStruct *pubkey_struct; + FILE *fp; + char *pubkey_s; + size_t pubkey_s_size; + size_t pubkey_struct_len; + + if ((fp = fopen(pk_file, "r")) == NULL) { + exit_err(pk_file); + } + if (fgets(pk_comment, (int) sizeof pk_comment, fp) == NULL) { + exit_err(pk_file); + } + pubkey_s_size = B64_MAX_LEN_FROM_BIN_LEN(sizeof *pubkey_struct) + 1U; + pubkey_s = xmalloc(pubkey_s_size); + if (fgets(pubkey_s, (int) pubkey_s_size, fp) == NULL) { + exit_err(pk_file); + } + trim(pubkey_s); + xfclose(fp); + pubkey_struct = xmalloc(sizeof *pubkey_struct); + if (b64_to_bin((unsigned char *) (void *) pubkey_struct, pubkey_s, + sizeof *pubkey_struct, strlen(pubkey_s), + &pubkey_struct_len) == NULL || + pubkey_struct_len != sizeof *pubkey_struct) { + exit_msg("base64 conversion failed"); + } + free(pubkey_s); + if (memcmp(pubkey_struct->sig_alg, SIGALG, + sizeof pubkey_struct->sig_alg) != 0) { + exit_msg("Unsupported signature algorithm"); + } + return pubkey_struct; +} + +static void +seckey_chk(unsigned char chk[crypto_generichash_BYTES], + const SeckeyStruct *seckey_struct) +{ + crypto_generichash_state hs; + + crypto_generichash_init(&hs, NULL, 0U, sizeof seckey_struct->keynum_sk.chk); + crypto_generichash_update(&hs, seckey_struct->keynum_sk.keynum, + sizeof seckey_struct->keynum_sk.keynum); + crypto_generichash_update(&hs, seckey_struct->keynum_sk.sk, + sizeof seckey_struct->keynum_sk.sk); + crypto_generichash_final(&hs, chk, sizeof seckey_struct->keynum_sk.chk); +} + +#ifndef VERIFY_ONLY +static SeckeyStruct * +seckey_load(const char *sk_file) +{ + char sk_comment[COMMENTMAXBYTES]; + unsigned char chk[crypto_generichash_BYTES]; + SeckeyStruct *seckey_struct; + FILE *fp; + char *pwd = xsodium_malloc(PASSWORDMAXBYTES); + char *seckey_s; + unsigned char *stream; + size_t seckey_s_size; + size_t seckey_struct_len; + + if ((fp = fopen(sk_file, "r")) == NULL) { + exit_err(sk_file); + } + if (fgets(sk_comment, (int) sizeof sk_comment, fp) == NULL) { + exit_err(sk_file); + } + sodium_memzero(sk_comment, sizeof sk_comment); + seckey_s_size = B64_MAX_LEN_FROM_BIN_LEN(sizeof *seckey_struct) + 1U; + seckey_s = xsodium_malloc(seckey_s_size); + seckey_struct = xsodium_malloc(sizeof *seckey_struct); + if (fgets(seckey_s, (int) seckey_s_size, fp) == NULL) { + exit_err(sk_file); + } + trim(seckey_s); + xfclose(fp); + if (b64_to_bin((unsigned char *) (void *) seckey_struct, seckey_s, + sizeof *seckey_struct, strlen(seckey_s), + &seckey_struct_len) == NULL || + seckey_struct_len != sizeof *seckey_struct) { + exit_msg("base64 conversion failed"); + } + sodium_free(seckey_s); + if (memcmp(seckey_struct->sig_alg, SIGALG, + sizeof seckey_struct->sig_alg) != 0) { + exit_msg("Unsupported signature algorithm"); + } + if (memcmp(seckey_struct->kdf_alg, KDFALG, + sizeof seckey_struct->kdf_alg) != 0) { + exit_msg("Unsupported key derivation function"); + } + if (memcmp(seckey_struct->chk_alg, CHKALG, + sizeof seckey_struct->chk_alg) != 0) { + exit_msg("Unsupported checksum function"); + } + if (get_password(pwd, PASSWORDMAXBYTES, "Password: ") != 0) { + exit_msg("get_password()"); + } + stream = xsodium_malloc(sizeof seckey_struct->keynum_sk); + if (crypto_pwhash_scryptsalsa208sha256 + (stream, sizeof seckey_struct->keynum_sk, pwd, strlen(pwd), + seckey_struct->kdf_salt, + le64_load(seckey_struct->kdf_opslimit_le), + le64_load(seckey_struct->kdf_memlimit_le)) != 0) { + abort(); + } + xor_buf((unsigned char *) (void *) &seckey_struct->keynum_sk, stream, + sizeof seckey_struct->keynum_sk); + sodium_free(stream); + seckey_chk(chk, seckey_struct); + if (memcmp(chk, seckey_struct->keynum_sk.chk, sizeof chk) != 0) { + exit_msg("Wrong password for that key"); + } + sodium_memzero(chk, sizeof chk); + + return seckey_struct; +} +#endif + +static int +verify(const char *pk_file, const char *message_file, const char *sig_file, + int quiet) +{ + char trusted_comment[TRUSTEDCOMMENTMAXBYTES]; + unsigned char global_sig[crypto_sign_BYTES]; + unsigned char *sig_and_trusted_comment; + SigStruct *sig_struct; + PubkeyStruct *pubkey_struct; + unsigned char *message; + size_t message_len; + size_t trusted_comment_len; + + pubkey_struct = pubkey_load(pk_file); + message = message_load(&message_len, message_file); + sig_struct = sig_load(sig_file, global_sig, + trusted_comment, sizeof trusted_comment); + if (memcmp(sig_struct->keynum, pubkey_struct->keynum_pk.keynum, + sizeof sig_struct->keynum) != 0) { + fprintf(stderr, "Signature key id in %s is %" PRIX64 "\n" + "but the key id in %s is %" PRIX64 "\n", + sig_file, le64_load(sig_struct->keynum), + pk_file, le64_load(pubkey_struct->keynum_pk.keynum)); + exit(1); + } + if (crypto_sign_verify_detached(sig_struct->sig, message, message_len, + pubkey_struct->keynum_pk.pk) != 0) { + if (quiet == 0) { + puts("Signature verification failed"); + } + exit(1); + } + free(message); + + trusted_comment_len = strlen(trusted_comment); + sig_and_trusted_comment = xmalloc((sizeof sig_struct->sig) + + trusted_comment_len); + memcpy(sig_and_trusted_comment, sig_struct->sig, sizeof sig_struct->sig); + memcpy(sig_and_trusted_comment + sizeof sig_struct->sig, trusted_comment, + trusted_comment_len); + if (crypto_sign_verify_detached(global_sig, sig_and_trusted_comment, + (sizeof sig_struct->sig) + trusted_comment_len, + pubkey_struct->keynum_pk.pk) != 0) { + if (quiet == 0) { + puts("Comment signature verification failed"); + } + exit(1); + } + free(sig_and_trusted_comment); + free(pubkey_struct); + free(sig_struct); + if (quiet == 0) { + puts("Signature and comment signature verified"); + printf("Trusted comment: %s\n", trusted_comment); + } + return 0; +} + +#ifndef VERIFY_ONLY +static int +sign(const char *sk_file, const char *message_file, const char *sig_file, + const char *comment, const char *trusted_comment) +{ + unsigned char global_sig[crypto_sign_BYTES]; + SigStruct sig_struct; + FILE *fp; + SeckeyStruct *seckey_struct; + unsigned char *message; + unsigned char *sig_and_trusted_comment; + size_t comment_len; + size_t trusted_comment_len; + size_t message_len; + + seckey_struct = seckey_load(sk_file); + message = message_load(&message_len, message_file); + memcpy(sig_struct.sig_alg, SIGALG, sizeof sig_struct.sig_alg); + memcpy(sig_struct.keynum, seckey_struct->keynum_sk.keynum, + sizeof sig_struct.keynum); + crypto_sign_detached(sig_struct.sig, NULL, message, message_len, + seckey_struct->keynum_sk.sk); + free(message); + if ((fp = fopen(sig_file, "w")) == NULL) { + exit_err(sig_file); + } + comment_len = strlen(comment); + assert(strrchr(comment, '\r') == NULL && strrchr(comment, '\n') == NULL); + assert(COMMENTMAXBYTES > sizeof COMMENT_PREFIX); + if (comment_len >= COMMENTMAXBYTES - sizeof COMMENT_PREFIX) { + fprintf(stderr, "Warning: comment too long. " + "This breaks compatibility with signify.\n"); + } + xfprintf(fp, "%s%s\n", COMMENT_PREFIX, comment); + xfput_b64(fp, (unsigned char *) (void *) &sig_struct, sizeof sig_struct); + + xfprintf(fp, "%s%s\n", TRUSTED_COMMENT_PREFIX, trusted_comment); + trusted_comment_len = strlen(trusted_comment); + assert(strrchr(trusted_comment, '\r') == NULL && + strrchr(trusted_comment, '\n') == NULL); + if (trusted_comment_len >= + TRUSTEDCOMMENTMAXBYTES - sizeof TRUSTED_COMMENT_PREFIX) { + exit_msg("Trusted comment too long"); + } + sig_and_trusted_comment = xmalloc((sizeof sig_struct.sig) + + trusted_comment_len); + memcpy(sig_and_trusted_comment, sig_struct.sig, sizeof sig_struct.sig); + memcpy(sig_and_trusted_comment + sizeof sig_struct.sig, trusted_comment, + trusted_comment_len); + crypto_sign_detached(global_sig, NULL, sig_and_trusted_comment, + (sizeof sig_struct.sig) + trusted_comment_len, + seckey_struct->keynum_sk.sk); + sodium_free(seckey_struct); + xfput_b64(fp, (unsigned char *) (void *) &global_sig, sizeof global_sig); + free(sig_and_trusted_comment); + xfclose(fp); + + return 0; +} + +static int +generate(const char *pk_file, const char *sk_file) +{ + char *pwd = xsodium_malloc(PASSWORDMAXBYTES); + char *pwd2 = xsodium_malloc(PASSWORDMAXBYTES); + SeckeyStruct *seckey_struct = xsodium_malloc(sizeof(SeckeyStruct)); + PubkeyStruct *pubkey_struct = xsodium_malloc(sizeof(PubkeyStruct)); + unsigned char *stream ; + FILE *fp; + + randombytes_buf(seckey_struct->keynum_sk.keynum, + sizeof seckey_struct->keynum_sk.keynum); + crypto_sign_keypair(pubkey_struct->keynum_pk.pk, + seckey_struct->keynum_sk.sk); + memcpy(seckey_struct->sig_alg, SIGALG, sizeof seckey_struct->sig_alg); + memcpy(seckey_struct->kdf_alg, KDFALG, sizeof seckey_struct->kdf_alg); + memcpy(seckey_struct->chk_alg, CHKALG, sizeof seckey_struct->chk_alg); + randombytes_buf(seckey_struct->kdf_salt, sizeof seckey_struct->kdf_salt); + le64_store(seckey_struct->kdf_opslimit_le, + crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_SENSITIVE); + le64_store(seckey_struct->kdf_memlimit_le, + crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_SENSITIVE); + seckey_chk(seckey_struct->keynum_sk.chk, seckey_struct); + memcpy(pubkey_struct->keynum_pk.keynum, seckey_struct->keynum_sk.keynum, + sizeof pubkey_struct->keynum_pk.keynum); + memcpy(pubkey_struct->sig_alg, SIGALG, sizeof pubkey_struct->sig_alg); + + if (get_password(pwd, PASSWORDMAXBYTES, "Password: ") != 0 || + get_password(pwd2, PASSWORDMAXBYTES, "Password (one more time): ") != 0) { + exit_msg("get_password()"); + } + if (strcmp(pwd, pwd2) != 0) { + exit_msg("Passwords don't match"); + } + stream = xsodium_malloc(sizeof seckey_struct->keynum_sk); + if (crypto_pwhash_scryptsalsa208sha256 + (stream, sizeof seckey_struct->keynum_sk, pwd, strlen(pwd), + seckey_struct->kdf_salt, + le64_load(seckey_struct->kdf_opslimit_le), + le64_load(seckey_struct->kdf_memlimit_le)) != 0) { + abort(); + } + sodium_free(pwd); + sodium_free(pwd2); + xor_buf((unsigned char *) (void *) &seckey_struct->keynum_sk, stream, + sizeof seckey_struct->keynum_sk); + sodium_free(stream); + + if ((fp = fopen_create_useronly(sk_file)) == NULL) { + exit_err(sk_file); + } + xfprintf(fp, "untrusted comment: minisign encrypted secret key\n"); + xfput_b64(fp, (unsigned char *) (void *) seckey_struct, + sizeof *seckey_struct); + xfclose(fp); + sodium_free(seckey_struct); + + if ((fp = fopen(pk_file, "w")) == NULL) { + exit_err(pk_file); + } + xfprintf(fp, "untrusted comment: minisign public key %" PRIX64 "\n", + le64_load(pubkey_struct->keynum_pk.keynum)); + xfput_b64(fp, (unsigned char *) (void *) pubkey_struct, + sizeof *pubkey_struct); + xfclose(fp); + sodium_free(pubkey_struct); + + return 0; +} +#endif + +static char * +append_sig_suffix(const char *message_file) +{ + char *sig_file; + size_t message_file_len = strlen(message_file); + + sig_file = xmalloc(message_file_len + sizeof SIG_SUFFIX); + memcpy(sig_file, message_file, message_file_len); + memcpy(sig_file + message_file_len, SIG_SUFFIX, sizeof SIG_SUFFIX); + + return sig_file; +} + +#ifndef VERIFY_ONLY +static char * +default_trusted_comment(const char *message_file) +{ + char *ret; + time_t ts = time(NULL); + + if (asprintf(&ret, "timestamp: %lu file: %s", + (unsigned long) ts, file_basename(message_file)) < 0 || + ret == NULL) { + exit_err("asprintf()"); + } + return ret; +} +#endif + +int +main(int argc, char **argv) +{ + const char *pk_file = SIG_DEFAULT_PKFILE; + const char *sk_file = SIG_DEFAULT_SKFILE; + const char *sig_file = NULL; + const char *message_file = NULL; + const char *comment = NULL; + const char *trusted_comment = NULL; + int opt_flag; + int quiet = 0; + Action action = ACTION_NONE; + + while ((opt_flag = getopt(argc, argv, getopt_options)) != -1) { + switch(opt_flag) { + case 'G': + if (action != ACTION_NONE && action != ACTION_GENERATE) { + usage(); + } + action = ACTION_GENERATE; + break; + case 'S': + if (action != ACTION_NONE && action != ACTION_SIGN) { + usage(); + } + action = ACTION_SIGN; + break; + case 'V': + if (action != ACTION_NONE && action != ACTION_VERIFY) { + usage(); + } + action = ACTION_VERIFY; + break; + case 'c': + comment = optarg; + break; + case 'h': + usage(); + case 'm': + message_file = optarg; + break; + case 'p': + pk_file = optarg; + break; + case 'q': + quiet = 1; + break; + case 's': + sk_file = optarg; + break; + case 't': + trusted_comment = optarg; + break; + case 'x': + sig_file = optarg; + break; + } + } + sodium_init(); + switch (action) { +#ifndef VERIFY_ONLY + case ACTION_GENERATE: + return generate(pk_file, sk_file) != 0; + case ACTION_SIGN: + if (message_file == NULL) { + usage(); + } + if (sig_file == NULL || *sig_file == 0) { + sig_file = append_sig_suffix(message_file); + } + if (comment == NULL || *comment == 0) { + comment = DEFAULT_COMMENT; + } + if (trusted_comment == NULL || *trusted_comment == 0) { + trusted_comment = default_trusted_comment(message_file); + } + return sign(sk_file, message_file, sig_file, comment, + trusted_comment) != 0; +#endif + case ACTION_VERIFY: + if (message_file == NULL) { + usage(); + } + if (sig_file == NULL || *sig_file == 0) { + sig_file = append_sig_suffix(message_file); + } + return verify(pk_file, message_file, sig_file, quiet) != 0; + default: + usage(); + } + return 0; +} diff --git a/src/minisign.h b/src/minisign.h new file mode 100644 index 0000000..e5b2662 --- /dev/null +++ b/src/minisign.h @@ -0,0 +1,58 @@ + +#ifndef MINISIGN_H +#define MINISIGN_H 1 + +#define COMMENTMAXBYTES 1024 +#define KEYNUMBYTES 8 +#define PASSWORDMAXBYTES 1024 +#define TRUSTEDCOMMENTMAXBYTES 8192 +#define SIGALG "Ed" +#define KDFALG "Sc" +#define CHKALG "B2" +#define COMMENT_PREFIX "untrusted comment: " +#define DEFAULT_COMMENT "signature from minisign secret key" +#define TRUSTED_COMMENT_PREFIX "trusted comment: " +#define SIG_DEFAULT_PKFILE "minisign.pub"; +#define SIG_DEFAULT_SKFILE "minisign.key"; +#define SIG_SUFFIX ".minisig" + +typedef struct KeynumSK_ { + unsigned char keynum[KEYNUMBYTES]; + unsigned char sk[crypto_sign_SECRETKEYBYTES]; + unsigned char chk[crypto_generichash_BYTES]; +} KeynumSK; + +typedef struct KeynumPK_ { + unsigned char keynum[KEYNUMBYTES]; + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; +} KeynumPK; + +typedef struct SeckeyStruct_ { + unsigned char sig_alg[2]; + unsigned char kdf_alg[2]; + unsigned char chk_alg[2]; + unsigned char kdf_salt[crypto_pwhash_scryptsalsa208sha256_SALTBYTES]; + unsigned char kdf_opslimit_le[8]; + unsigned char kdf_memlimit_le[8]; + KeynumSK keynum_sk; +} SeckeyStruct; + +typedef struct PubkeyStruct_ { + unsigned char sig_alg[2]; + KeynumPK keynum_pk; +} PubkeyStruct; + +typedef struct SigStruct_ { + unsigned char sig_alg[2]; + unsigned char keynum[KEYNUMBYTES]; + unsigned char sig[crypto_sign_BYTES]; +} SigStruct; + +typedef enum Action_ { + ACTION_NONE, + ACTION_GENERATE, + ACTION_SIGN, + ACTION_VERIFY +} Action; + +#endif