From 617f6e79adfdf9780ff4011b0499292522c40d6a Mon Sep 17 00:00:00 2001 From: Duncan Overbruck Date: Sat, 25 Feb 2023 23:01:49 +0100 Subject: [PATCH] bin/xbps-sign: wip --- bin/Makefile | 1 + bin/xbps-sign/Makefile | 6 + bin/xbps-sign/main.c | 401 +++++++++++++++++++++++++++++ tests/xbps/Kyuafile | 1 + tests/xbps/Makefile | 15 +- tests/xbps/xbps-sign/Kyuafile | 4 + tests/xbps/xbps-sign/Makefile | 8 + tests/xbps/xbps-sign/basic_test.sh | 132 ++++++++++ 8 files changed, 567 insertions(+), 1 deletion(-) create mode 100644 bin/xbps-sign/Makefile create mode 100644 bin/xbps-sign/main.c create mode 100644 tests/xbps/xbps-sign/Kyuafile create mode 100644 tests/xbps/xbps-sign/Makefile create mode 100644 tests/xbps/xbps-sign/basic_test.sh diff --git a/bin/Makefile b/bin/Makefile index c0586aabb..833a3de30 100644 --- a/bin/Makefile +++ b/bin/Makefile @@ -14,6 +14,7 @@ SUBDIRS += xbps-checkvers SUBDIRS += xbps-fbulk SUBDIRS += xbps-digest SUBDIRS += xbps-fetch +SUBDIRS += xbps-sign ifeq (${XBPS_OS},linux) SUBDIRS += xbps-uchroot diff --git a/bin/xbps-sign/Makefile b/bin/xbps-sign/Makefile new file mode 100644 index 000000000..b6a470c6c --- /dev/null +++ b/bin/xbps-sign/Makefile @@ -0,0 +1,6 @@ +TOPDIR = ../.. +-include $(TOPDIR)/config.mk + +BIN = xbps-sign + +include $(TOPDIR)/mk/prog.mk diff --git a/bin/xbps-sign/main.c b/bin/xbps-sign/main.c new file mode 100644 index 000000000..ff6458e02 --- /dev/null +++ b/bin/xbps-sign/main.c @@ -0,0 +1,401 @@ +/*- + * Copyright (c) 2023 Duncan Overbruck . + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * Copyright (c) 2015-2018 + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define PASSPHRASE_MAX_BYTES 1024 + +static const char *comment = NULL; +static const char *pubkey_file = NULL; +static const char *pubkey_s = NULL; +static const char *seckey_file = NULL; +static const char *passphrase_file = NULL; +static const char *msg_file = NULL; +static const char *sig_file = NULL; + +static void __attribute__((noreturn)) +usage(void) +{ + fprintf(stdout, + "Usage: xbps-sign MODE OPTIONS\n" + " xbps-sign -G [-c comment] -p pubkey -s seckey\n" + " xbps-sign -S [-x sigfile] -s seckey -m file\n" + " xbps-sign -V [-x sigfile] [-p pubkey] -m file\n" + "\nMODE\n" + " -G --generate Generate a new key pair\n" + " -S --sign Sign a file\n" + " -V --verify Verify a file\n" + " -h --help Print help usage\n" + " --version Prints the xbps release version\n" + "\nOPTIONS\n" + " -m --message Message file to sign/verify\n" + " -p --pubkey Public-key file\n" + " -s --seckey Secret-key file\n" + " -x --signature Signature file (default .minisig)\n" + " -c --comment Untrusted comment\n" + " --passphrase Passphrase file\n" + ""); + exit(EXIT_FAILURE); +} + +static const char * +read_passphrase(char *buf, size_t bufsz) +{ + if (passphrase_file) { + FILE *fp; + int r = 0; + + fp = fopen(passphrase_file, "r"); + if (!fp) { + xbps_error_printf("failed to open passphrase file: %s: %s\n", + passphrase_file, strerror(errno)); + exit(1); + } + if (!fgets(buf, bufsz, fp)) + r = -EINVAL; + if (fclose(fp) != 0) + r = -errno; + if (r < 0) { + xbps_error_printf("failed to read passphrase file: %s: %s\n", + passphrase_file, strerror(-r)); + exit(1); + } + return buf; + } + xbps_warn_printf("generating unencrypted secret-key\n"); + return NULL; +} + + +static void __attribute__((noreturn)) +generate(void) +{ + char passphrase_buf[PASSPHRASE_MAX_BYTES]; + struct xbps_pubkey pubkey = {0}; + struct xbps_seckey seckey = {0}; + const char *passphrase = NULL; + int r; + + if (!seckey_file) { + xbps_error_printf("missing secret-key path\n"); + exit(1); + } + + passphrase = read_passphrase(passphrase_buf, sizeof(passphrase_buf)); + + r = xbps_generate_keypair(&seckey, &pubkey); + if (r < 0) { + exit(1); + } + + r = xbps_seckey_write(&seckey, passphrase, seckey_file); + if (passphrase) + xbps_wipe_secret(passphrase_buf, sizeof(passphrase_buf)); + xbps_wipe_secret(&seckey, sizeof(seckey)); + if (r < 0) { + xbps_error_printf("failed to write secret-key file: %s: %s\n", + seckey_file, strerror(-r)); + exit(1); + } + + if (pubkey_file) { + r = xbps_pubkey_write(&pubkey, pubkey_file); + if (r < 0) { + xbps_warn_printf("failed to write public-key file: %s: %s\n", + pubkey_file, strerror(-r)); + exit(1); + } + } + + exit(0); +} + +static void +load_pubkey(struct xbps_pubkey *pubkey) +{ + if (pubkey_file) { + int fd; + int r; + fd = open(pubkey_file, O_RDONLY); + if (fd == -1) { + xbps_error_printf("failed to open public-key file: %s: %s\n", + pubkey_file, strerror(errno)); + exit(1); + } + r = xbps_pubkey_read(pubkey, fd); + close(fd); + if (r < 0) { + xbps_error_printf("failed to read public-key file: %s: %s\n", + pubkey_file, strerror(-r)); + exit(1); + } + } else if (pubkey_s) { + int r = xbps_pubkey_decode(pubkey, pubkey_s); + if (r < 0) { + xbps_error_printf("failed to decode public-key: %s\n", strerror(-r)); + exit(1); + } + } else { + xbps_error_printf("missing public-key\n"); + exit(1); + } +} + +static void +load_seckey(struct xbps_seckey *seckey) +{ + char passphrase_buf[PASSPHRASE_MAX_BYTES]; + const char *passphrase = NULL; + int r; + + if (!seckey_file) { + xbps_error_printf("missing secret-key\n"); + exit(1); + } + if (passphrase_file) + exit(1); + r = xbps_seckey_read(seckey, passphrase, seckey_file); + if (passphrase) + xbps_wipe_secret(passphrase_buf, sizeof(passphrase_buf)); + if (r < 0) { + xbps_error_printf("failed to read secret-key file: %s: %s\n", + seckey_file, strerror(-r)); + exit(1); + } +} + +static void __attribute__((noreturn)) +sign(void) +{ + char sigfile_buf[PATH_MAX]; + struct xbps_seckey seckey = {0}; + struct xbps_pubkey pubkey = {0}; + struct xbps_minisig minisig = {0}; + struct xbps_hash hash; + int r; + + if (!msg_file) { + xbps_error_printf("missing file to sign\n"); + exit(1); + } + + if (pubkey_file || pubkey_s) + load_pubkey(&pubkey); + + r = xbps_hash_file(&hash, msg_file); + if (r < 0) { + xbps_error_printf("failed to hash file: %s: %s\n", + msg_file, strerror(-r)); + exit(1); + } + + xbps_strlcpy(minisig.comment, "signature from minisign secret-key", sizeof(minisig.comment)); + snprintf(minisig.trusted_comment, sizeof(minisig.trusted_comment), + "foo bar"); + + load_seckey(&seckey); + + r = xbps_minisig_sign(&minisig, &seckey, &hash); + if (r < 0) { + xbps_wipe_secret(&seckey, sizeof(seckey)); + xbps_error_printf("failed to sign file: %s: %s\n", + seckey_file, strerror(errno)); + exit(1); + } + xbps_wipe_secret(&seckey, sizeof(seckey)); + + if (pubkey_file || pubkey_s) { + r = xbps_minisig_verify(&minisig, &pubkey, &hash); + if (r < 0) { + xbps_error_printf("failed to verify generated signature: %s\n", + strerror(-r)); + exit(1); + } + } + + if (!sig_file) { + snprintf(sigfile_buf, sizeof(sigfile_buf), "%s.minisig", msg_file); + sig_file = sigfile_buf; + } + r = xbps_minisig_write(&minisig, sig_file); + if (r < 0) { + xbps_error_printf("failed to write signature file: %s: %s\n", + sig_file, strerror(-r)); + exit(1); + } + exit(0); +} + +static void __attribute__((noreturn)) +verify(void) +{ + char sigfile_buf[PATH_MAX]; + struct xbps_pubkey pubkey = {0}; + struct xbps_hash hash; + struct xbps_minisig minisig; + int r; + + load_pubkey(&pubkey); + + r = xbps_hash_file(&hash, msg_file); + if (r < 0) { + xbps_error_printf("failed to hash file: %s: %s\n", + msg_file, strerror(-r)); + exit(1); + } + if (!sig_file) { + snprintf(sigfile_buf, sizeof(sigfile_buf), "%s.minisig", msg_file); + sig_file = sigfile_buf; + } + r = xbps_minisig_read(&minisig, sig_file); + if (r < 0) { + xbps_error_printf("failed to read minisig file: %s: %s\n", + sig_file, strerror(-r)); + exit(1); + } + fprintf(stderr, "untrusted comment: %s\n", minisig.comment); + fprintf(stderr, "trusted comment: %s\n", minisig.trusted_comment); + r = xbps_minisig_verify(&minisig, &pubkey, &hash); + if (r < 0) { + xbps_error_printf("failed to verify file: %s: %s\n", + msg_file, strerror(-r)); + exit(1); + } + exit(0); +} + +int +main(int argc, char *argv[]) +{ + const char *shortopts = "dGHSVhc:p:s:m:x:P:"; + const struct option longopts[] = { + { "generate", no_argument, NULL, 'G' }, + { "sign", no_argument, NULL, 'S' }, + { "verify", no_argument, NULL, 'V' }, + { "message", no_argument, NULL, 'm' }, + { "seckey", required_argument, NULL, 's' }, + { "pubkey-file", required_argument, NULL, 'p' }, + { "pubkey", required_argument, NULL, 'P' }, + { "comment", required_argument, NULL, 'c' }, + { "passphrase-file", required_argument, NULL, 1 }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 0 }, + { "debug", no_argument, NULL, 'd' }, + { NULL, 0, NULL, 0 } + }; + int c; + + enum { + GENERATE = 1, + SIGN, + VERIFY, + } act = 0; + + (void) comment; + + while ((c = getopt_long(argc, argv, shortopts, longopts, NULL)) != -1) { + switch (c) { + case 'd': + xbps_debug_level = 1; + break; + case 'G': + act = GENERATE; + break; + case 'V': + act = VERIFY; + break; + case 'c': + comment = optarg; + break; + case 'p': + pubkey_file = optarg; + break; + case 'P': + pubkey_s = optarg; + break; + case 's': + seckey_file = optarg; + break; + case 'S': + act = SIGN; + break; + case 'x': + sig_file = optarg; + break; + case 'm': + msg_file = optarg; + break; + case '?': + case 'h': + usage(); + /* NOTREACHED */ + case 0: + printf("%s\n", XBPS_RELVER); + exit(EXIT_SUCCESS); + case 1: + passphrase_file = optarg; + break; + default: + usage(); + /* NOTREACHED */ + } + } + + switch (act) { + case GENERATE: generate(); break; + case SIGN: sign(); break; + case VERIFY: verify(); break; + default: + usage(); + } + exit(1); +} diff --git a/tests/xbps/Kyuafile b/tests/xbps/Kyuafile index f1e22c8b5..0bfd739f4 100644 --- a/tests/xbps/Kyuafile +++ b/tests/xbps/Kyuafile @@ -13,3 +13,4 @@ include('xbps-rindex/Kyuafile') include('xbps-uhelper/Kyuafile') include('xbps-remove/Kyuafile') include('xbps-digest/Kyuafile') +include('xbps-sign/Kyuafile') diff --git a/tests/xbps/Makefile b/tests/xbps/Makefile index 840fa4c63..fd98417a6 100644 --- a/tests/xbps/Makefile +++ b/tests/xbps/Makefile @@ -1,5 +1,18 @@ -include ../../config.mk -SUBDIRS = common libxbps xbps-alternatives xbps-checkvers xbps-create xbps-fetch xbps-install xbps-query xbps-rindex xbps-uhelper xbps-remove xbps-digest +SUBDIRS = \ + common \ + libxbps \ + xbps-alternatives \ + xbps-checkvers \ + xbps-create \ + xbps-fetch \ + xbps-install \ + xbps-query \ + xbps-rindex \ + xbps-uhelper \ + xbps-remove \ + xbps-digest \ + xbps-sign include ../../mk/subdir.mk diff --git a/tests/xbps/xbps-sign/Kyuafile b/tests/xbps/xbps-sign/Kyuafile new file mode 100644 index 000000000..a9aa2180f --- /dev/null +++ b/tests/xbps/xbps-sign/Kyuafile @@ -0,0 +1,4 @@ +syntax("kyuafile", 1) + +test_suite("xbps-sign") +atf_test_program{name="basic_test"} diff --git a/tests/xbps/xbps-sign/Makefile b/tests/xbps/xbps-sign/Makefile new file mode 100644 index 000000000..76395cf9c --- /dev/null +++ b/tests/xbps/xbps-sign/Makefile @@ -0,0 +1,8 @@ +TOPDIR = ../../.. +-include $(TOPDIR)/config.mk + +TESTSHELL = basic_test +TESTSSUBDIR = xbps/xbps-sign +EXTRA_FILES = Kyuafile + +include $(TOPDIR)/mk/test.mk diff --git a/tests/xbps/xbps-sign/basic_test.sh b/tests/xbps/xbps-sign/basic_test.sh new file mode 100644 index 000000000..d11221694 --- /dev/null +++ b/tests/xbps/xbps-sign/basic_test.sh @@ -0,0 +1,132 @@ +#! /usr/bin/env atf-sh +# Test that xbps-sign(1) works as expected. + +atf_test_case generate_errors + +errors_head() { + atf_set "descr" "xbps-sign(1): error conditions" +} + +run() { + local e="ignore" + local o="ignore" + local s + while getopts e:o:s: f; do + case "$f" in + e) e="$OPTARG" ;; + o) o="$OPTARG" ;; + s) s="$OPTARG" ;; + esac + done + shift $(( OPTIND - 1 )) + # atf_check ${s+-s "$s"} -e "$e" -o "$o" -- gdb --return-child-result -q -batch -ex r -ex bt -args "$@" + ${Atf_Check} ${s+-s "$s"} -e "$e" -o "$o" -- "$@" + local rv=$? + if [ $rv != 0 ]; then + [ -f core.* ] && gdb -q -batch -ex r -ex 'bt f' "$1" core.* + atf_fail "atf-check failed; see the output of the test for details" + fi +} + +errors_body() { + touch existing + + # generate + atf_check -s exit:1 -e match:"missing secret-key" xbps-sign -G + atf_check -s exit:1 -e match:"missing secret-key" xbps-sign -G -p test.pub + # atf_check -s exit:1 -e match:"missing public" xbps-sign -G -s test.key + atf_check -s exit:1 -e match:"existing" xbps-sign -G -s existing -p test.pub + # atf_check -s exit:1 -e match:"existing" xbps-sign -G -s test.key -p existing + # atf_check -s exit:1 -e match:"failed to read passphrase file" \ + # xbps-sign -G -s test.key -p test.pub --passphrase-file fail + + # sign + atf_check -s exit:1 -e match:"missing secret-key" xbps-sign -S -m existing + atf_check -s exit:1 -e match:"missing file to sign" xbps-sign -S -s existing + atf_check -s exit:1 -e match:"failed to read secret-key file" xbps-sign -S -s missing -m existing + atf_check -s exit:1 -e match:"failed to hash file" xbps-sign -S -s existing -m missing + + #verify +} + +atf_test_case unencrypted + +unencrypted_head() { + atf_set "descr" "xbps-sign(1): generate and use unencrypted key pair" +} + +unencrypted_body() { + echo abc123 >testfile + echo 123abc >badfile + run -e match:"unencrypted" xbps-sign -G -s test.key -p test.pub + run test -r test.key + # run test -r test.pub + vis test.key + hexdump test.key + run valgrind xbps-sign -d -S -p test.pub -s test.key -m testfile + ls -lsa + head -v testfile.minisig + echo ">> minisign:" + minisign -V -p test.pub -m testfile + echo "<<" + run valgrind xbps-sign -d -V -p test.pub -m testfile + run -s exit:1 xbps-sign -d -V -p test.pub -m badfile -x testfile.minisig + # minisign -V -p test.pub -m testfile + # run -s exit:1 wc testfile.minisig + cat testfile.minisig + base64 < testfile.minisig | wc + + # run xbps-sign -G -s test1.key -p test1.pub + # test -r test1.key || atf_fail "test1.key does not exist" + # test -r test1.pub || atf_fail "test1.pub does not exist" + # run -s exit:0 xbps-sign -d -V -p test1.pub -m testfile +} + +atf_test_case encrypted + +encrypted_head() { + atf_set "descr" "xbps-sign(1): generate and use encrypted key pair" +} + +encrypted_body() { + atf_skip "wtf not fully implemented" + echo "abc123" > passphrase + atf_check -s exit:0 xbps-sign -G -s test.key -p test.pub --passphrase-file passphrase + test -r test.key || atf_fail "test.key does not exist" + test -r test.pub || atf_fail "test.pub does not exist" + atf_check -s exit:0 xbps-sign -S -s test.key +} + +atf_test_case minisign + +minisign_head() { + atf_set "descr" "xbps-sign(1): test minisign compatibility" +} + +minisign_body() { + atf_require_prog minisign + echo "abc123" > test + + # minisign verify using xbps-sign generated keypair and signature + run xbps-sign -G -s test.key -p test.pub + run xbps-sign -S -s test.key -m test + run minisign -V -p test.pub -m test + + # minisign sign using xbps-generated keypair and verify using xbps-sign + rm test.minisig + run minisign -S -s test.key -m test + run xbps-sign -V -p test.pub -m test + + # minising generated keypair + rm test.pub test.key test.minisig + run minisign -GW -s test.key -p test.pub + run minisign -S -s test.key -m test + run xbps-sign -V -p test.pub -m test +} + +atf_init_test_cases() { + atf_add_test_case errors + atf_add_test_case unencrypted + atf_add_test_case encrypted + atf_add_test_case minisign +}