diff --git a/Makefile.am b/Makefile.am index 1cd009811..d091b4850 100644 --- a/Makefile.am +++ b/Makefile.am @@ -35,7 +35,8 @@ ACLOCAL_AMFLAGS = -I m4 AM_DISTCHECK_DVI_TARGET = pdf AM_DISTCHECK_CONFIGURE_FLAGS = --enable-gnupg-builddir-envvar \ --enable-all-tests --enable-g13 \ - --enable-gpgtar --enable-wks-tools --disable-ntbtls + --enable-gpgtar --enable-wks-tools --disable-ntbtls \ + --enable-gpgsum GITLOG_TO_CHANGELOG=gitlog-to-changelog diff --git a/autogen.rc b/autogen.rc index a1eb96931..05d6759f5 100644 --- a/autogen.rc +++ b/autogen.rc @@ -10,7 +10,7 @@ case "$myhost:$myhostsub" in extraoptions="$extraoptions --disable-zip" ;; w32:) - extraoptions="--enable-gpgtar" + extraoptions="--enable-gpgtar --enable-gpgsum" ;; esac diff --git a/configure.ac b/configure.ac index 8f09bb06a..1b9af5f1a 100644 --- a/configure.ac +++ b/configure.ac @@ -136,6 +136,7 @@ GNUPG_BUILD_PROGRAM(gpgtar, yes) # We also install the gpg-wks-server tool by default but disable it # later for platforms where it can't be build. GNUPG_BUILD_PROGRAM(wks-tools, yes) +GNUPG_BUILD_PROGRAM(gpgsum, yes) AC_SUBST(PACKAGE) @@ -1849,6 +1850,7 @@ AM_CONDITIONAL(BUILD_TPM2D, test "$build_tpm2d" = "yes") AM_CONDITIONAL(BUILD_DOC, test "$build_doc" = "yes") AM_CONDITIONAL(BUILD_GPGTAR, test "$build_gpgtar" = "yes") AM_CONDITIONAL(BUILD_WKS_TOOLS, test "$build_wks_tools" = "yes") +AM_CONDITIONAL(BUILD_GPGSUM, test "$build_gpgsum" = "yes") AM_CONDITIONAL(DISABLE_TESTS, test "$run_tests" != yes) AM_CONDITIONAL(ENABLE_CARD_SUPPORT, test "$card_support" = yes) @@ -1923,6 +1925,7 @@ AC_DEFINE_UNQUOTED(GPGCONF_DISP_NAME, "GPGConf", [The displayed name of gpgconf]) AC_DEFINE_UNQUOTED(GPGTAR_NAME, "gpgtar", [The name of the gpgtar tool]) +AC_DEFINE_UNQUOTED(GPGSUM_NAME, "gpgsum", [The name of the gpgsum tool]) AC_DEFINE_UNQUOTED(GPG_AGENT_SOCK_NAME, "S.gpg-agent", [The name of the agent socket]) @@ -2118,6 +2121,7 @@ tools/gpgtar.w32-manifest tools/gpg-check-pattern.w32-manifest tools/gpg-wks-client.w32-manifest tools/gpg-card.w32-manifest +tools/gpgsum.w32-manifest ]) @@ -2144,6 +2148,7 @@ echo " Keyboxd: $build_keyboxd Gpgtar: $build_gpgtar WKS tools: $build_wks_tools + Gpgsum: $build_gpgsum Protect tool: $show_gnupg_protect_tool_pgm LDAP wrapper: $show_gnupg_dirmngr_ldap_pgm diff --git a/tools/Makefile.am b/tools/Makefile.am index 769a81a00..c31cba835 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -26,7 +26,8 @@ EXTRA_DIST = \ gpgtar-w32info.rc gpgtar.w32-manifest.in \ gpg-check-pattern-w32info.rc gpg-check-pattern.w32-manifest.in \ gpg-wks-client-w32info.rc gpg-wks-client.w32-manifest.in \ - gpg-card-w32info.rc gpg-card.w32-manifest.in + gpg-card-w32info.rc gpg-card.w32-manifest.in \ + gpgsum-w32info.rc gpgsum.w32-manifest.in AM_CPPFLAGS = include $(top_srcdir)/am/cmacros.am @@ -38,6 +39,7 @@ gpg_card_rc_objs = gpg-card-w32info.o gpgtar_rc_objs = gpgtar-w32info.o gpg_check_pattern_rc_objs = gpg-check-pattern-w32info.o gpg_wks_client_rc_objs = gpg-wks-client-w32info.o +gpgsum_rc_objs = gpgsum-w32info.o gpg-connect-agent-w32info.o : gpg-connect-agent.w32-manifest \ ../common/w32info-rc.h @@ -48,6 +50,7 @@ gpg-check-pattern-w32info.o : gpg-check-pattern.w32-manifest \ ../common/w32info-rc.h gpg-wks-client-w32info.o : gpg-wks-client.w32-manifest \ ../common/w32info-rc.h +gpgsum-w32info.o : gpgsum.w32-manifest ../common/w32info-rc.h endif @@ -82,6 +85,12 @@ else noinst_PROGRAMS += gpgtar endif +if BUILD_GPGSUM + bin_PROGRAMS += gpgsum +else + noinst_PROGRAMS += gpgsum +endif + common_libs = $(libcommon) commonpth_libs = $(libcommonpth) @@ -157,6 +166,14 @@ gpgtar_LDADD = $(libcommon) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) \ $(LIBINTL) $(NETLIBS) $(LIBICONV) $(W32SOCKLIBS) \ $(gpgtar_rc_objs) +gpgsum_SOURCES = \ + gpgsum.c + +gpgsum_CFLAGS = $(LIBGCRYPT_CFLAGS) $(GPG_ERROR_CFLAGS) +gpgsum_LDADD = $(libcommon) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) \ + $(LIBINTL) $(NETLIBS) \ + $(gpgsum_rc_objs) + gpg_wks_server_SOURCES = \ gpg-wks-server.c \ gpg-wks.h \ diff --git a/tools/gpgsum-w32info.rc b/tools/gpgsum-w32info.rc new file mode 100644 index 000000000..be551ab4d --- /dev/null +++ b/tools/gpgsum-w32info.rc @@ -0,0 +1,52 @@ +/* gpgsum-w32info.rc -*- c -*- + * Copyright (C) 2020-2023 g10 Code GmbH + * + * This file is free software; as a special exception the author gives + * unlimited permission to copy and/or distribute it, with or without + * modifications, as long as this notice is preserved. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY, to the extent permitted by law; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ + +#include "afxres.h" +#include "../common/w32info-rc.h" + +1 ICON "../common/gnupg.ico" + +1 VERSIONINFO + FILEVERSION W32INFO_VI_FILEVERSION + PRODUCTVERSION W32INFO_VI_PRODUCTVERSION + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x01L /* VS_FF_DEBUG (0x1)*/ +#else + FILEFLAGS 0x00L +#endif + FILEOS 0x40004L /* VOS_NT (0x40000) | VOS__WINDOWS32 (0x4) */ + FILETYPE 0x1L /* VFT_APP (0x1) */ + FILESUBTYPE 0x0L /* VFT2_UNKNOWN */ + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" /* US English (0409), Unicode (04b0) */ + BEGIN + VALUE "FileDescription", L"GnuPG\x2019s hashsum tool\0" + VALUE "InternalName", "gpgsum\0" + VALUE "OriginalFilename", "gpgsum.exe\0" + VALUE "ProductName", W32INFO_PRODUCTNAME + VALUE "ProductVersion", W32INFO_PRODUCTVERSION + VALUE "CompanyName", W32INFO_COMPANYNAME + VALUE "FileVersion", W32INFO_FILEVERSION + VALUE "LegalCopyright", W32INFO_LEGALCOPYRIGHT + VALUE "Comments", W32INFO_COMMENTS + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 0x4b0 + END + END + +1 RT_MANIFEST "gpgsum.w32-manifest" diff --git a/tools/gpgsum.c b/tools/gpgsum.c new file mode 100644 index 000000000..967b40c19 --- /dev/null +++ b/tools/gpgsum.c @@ -0,0 +1,528 @@ +/* gpgsum.c - A simple hash sum tool mainly useful for Windows. + * Copyright (C) 2023 g10 Code GmbH + * + * This file is part of GnuPG. + * + * GnuPG is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * GnuPG is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#include + +#include +#include +#include +#include +#include +#ifdef _WIN32 +# include +# include +#endif + +#define INCLUDED_BY_MAIN_MODULE 1 +#include "../common/util.h" +#include "../common/init.h" +#include "../common/i18n.h" +#include "../common/sysutils.h" + +#include + +static unsigned int filecount; +static unsigned int readerrors; +static unsigned int checkcount; +static unsigned int matcherrors; + +struct { + int algo; + int check; + int filenames; +} opt; + +enum cmd_and_opt_values + { + aNull = 0, + aCheck = 'c', + oFileNamesFromStdIn = '0', + }; + +static gpgrt_opt_t opts[] = { + ARGPARSE_group (300, N_("@Commands:\n ")), + + ARGPARSE_c (aCheck, "check", + N_("Read checksums from files and check them")), + ARGPARSE_group (301, N_("@\nOptions:\n ")), + ARGPARSE_s_n (oFileNamesFromStdIn, "filenames", + N_("Read file names from stdin")), + ARGPARSE_end () +}; + +static void +parse_arguments (gpgrt_argparse_t *pargs, gpgrt_opt_t *popts) +{ + while (gpgrt_argparse (NULL, pargs, popts)) + { + switch (pargs->r_opt) + { + case aCheck: + opt.check = 1; + break; + case oFileNamesFromStdIn: + opt.filenames = 1; + break; + default: pargs->err = 2; break; + } + } +} + +static const char * +my_strusage( int level ) +{ + const char *p; + + switch (level) + { + case 9: p = "GPL-3.0-or-later"; break; + case 11: p = "gpgsum (@GNUPG@)"; + break; + case 13: p = VERSION; break; + //case 14: p = GNUPG_DEF_COPYRIGHT_LINE; break; + //case 17: p = PRINTABLE_OS_NAME; break; + case 19: p = _("Please report bugs to <@EMAIL@>.\n"); break; + + case 1: + case 40: + p = _("Usage: gpgsum [-c|-0] [--] FILENAMES|-"); + break; + case 41: + p = _("Syntax: gpgsum [-c|-0] [--] FILENAMES|-\n" + "Create or verify hash sums for files.\n" + "The executable must be named for the desired hash algorithm " + "(i.e., \"sha2sum\")."); + break; + + default: p = NULL; break; + } + return p; +} + +/* We need to escape the fname so that included linefeeds etc don't + mess up the the output file. On windows we also turn backslashes + into slashes so that we don't get into conflicts with the escape + character. Note that the GNU version escapes the backslash and the + LF but we also escape the CR. */ +static char * +escapefname (const char *fname, int *escaped) +{ + const char *s; + char *buffer; + char *d; + size_t n; + + *escaped = 0; + for (n = 0, s = fname; *s; s++) + { + if (*s == '\n' || *s == '\r') + n += 2; + else if (*s == '\\') + { +#ifdef _WIN32 + n++; +#else + n += 2; +#endif + } + else + n++; + } + n++; + buffer = xmalloc (n); + d = buffer; + for (s = fname; *s; s++) + { + if (*s == '\n') + { + *d++ = '\\'; + *d++ = 'n' ; + *escaped = 1; + } + else if (*s == '\r') + { + *d++ = '\\'; + *d++ = 'r' ; + *escaped = 1; + } + else if (*s == '\\') + { +#ifdef _WIN32 + *d++ = '/'; +#else + *d++ = '\\'; + *d++ = '\\' ; + *escaped = 1; +#endif + } + else + *d++ = *s; + } + *d = 0; + return buffer; +} + + +/* Revert the escaping in-place. We handle some more of the standard + escaping characters but not all. */ +static void +unescapefname (char *fname) +{ + char *s, *d; + + for (s=d=fname; *s; s++) + { + if (*s == '\\' && s[1]) + { + s++; + switch (*s) + { + case '\\': *d++ = '\\'; break; + case 'n': *d++ = '\n'; break; + case 'r': *d++ = '\r'; break; + case 'f': *d++ = '\f'; break; + case 'v': *d++ = '\v'; break; + case 'b': *d++ = '\b'; break; + default: *d++ = '\\'; *d++ = *s; break; + } + } + else + *d++ = *s; + } + *d = 0; +} + +static gpg_error_t +hash_file (const char *fname, const char *expected) +{ + gpg_error_t err; + estream_t fp; + char buffer[4096]; + size_t n; + char *p; + char *fnamebuf; + int escaped; + gcry_md_hd_t hd; + unsigned char *result; + unsigned int digest_length; + + + digest_length = gcry_md_get_algo_dlen(opt.algo); + + filecount++; + if (!expected && *fname == '-' && !fname[1]) + { + /* Not in check mode and asked to read from stdin. */ + fp = es_stdin; + es_set_binary (es_stdin); + } + else + fp = es_fopen (fname, "rb"); + + if (!fp) + { + err = gpg_error_from_syserror (); + log_error ("Can't open '%s': %s\n", + fname, gpg_strerror (err)); + if (expected) + log_error ("%s: FAILED open\n", fname); + readerrors++; + return gpg_error(GPG_ERR_GENERAL); + } + + err = gcry_md_open(&hd, opt.algo, 0); + if (err) { + log_error("Failed to open md: %s\n", gcry_strerror(err)); + } + + while ( (n = es_fread (buffer, 1, sizeof buffer, fp))) + gcry_md_write(hd, buffer, n); + if (es_ferror (fp)) + { + log_error ("Error reading `%s': %s\n", + fname, strerror (errno)); + if (fp != es_stdin) + es_fclose (fp); + if (expected) + es_printf ("%s: FAILED read\n", fname); + readerrors++; + return gpg_error(GPG_ERR_GENERAL); + } + if (fp != es_stdin) + es_fclose (fp); + + fnamebuf = escapefname (fname, &escaped); + fname = fnamebuf; + + result = gcry_md_read(hd, opt.algo); + if (!result) { + log_error("Failed to read digest\n"); + return gpg_error(GPG_ERR_GENERAL); + } + checkcount++; + bin2hex(result, digest_length, buffer); + if (expected) + { + /* Lowercase the checksum. */ + buffer[strlen(expected)] = 0; + for (p=buffer; *p; p++) + if (*p >= 'A' && *p <= 'Z') + *p |= 0x20; + if (strcmp (buffer, expected)) + { + es_printf ("%s: FAILED\n", fname); + matcherrors++; + return -1; + } + es_printf ("%s: OK\n", fname); + } + else + es_printf ("%s%s %s\n", escaped? "\\":"", buffer, fname); + xfree (fnamebuf); + return 0; +} + +static gpg_error_t +check_file (const char *fname) +{ + estream_t fp; + char *linebuf = NULL; + char *line; + char *p; + size_t n; + int rc = 0; + int escaped; + unsigned int digest_length; + unsigned int name_offset; + size_t line_length; + size_t max_length; + + digest_length = gcry_md_get_algo_dlen(opt.algo); + name_offset = digest_length * 2 + 2; + + if (*fname == '-' && !fname[1]) + fp = es_stdin; + else + fp = es_fopen (fname, "r"); + if (!fp) + { + log_error ("Can't open '%s': %s\n", fname, strerror(errno)); + return -1; + } + + max_length = 4096; + while ( es_read_line (fp, &linebuf, &line_length, &max_length) ) + { + escaped = (*linebuf == '\\'); + line = linebuf + escaped; + n = strlen(line); + if (!n || line[n-1] != '\n') + { + log_error ("Error reading '%s': %s\n", fname, + es_feof (fp)? "last linefeed missing":"line too long"); + rc = -1; + break; + } + line[--n] = 0; + if (n && line[n-1] == '\r') + line[--n] = 0; + if (!*line) + continue; /* Ignore empty lines. */ + if (n < name_offset || line[name_offset-2] != ' ') + { + fprintf (stderr, "Error parsing `%s': %s\n", fname, + "invalid line"); + rc = -1; + continue; + } + + /* Note that we ignore the binary flag ('*') used by GNU + versions of this tool: It does not make sense to compute a + digest over some transformation of a file - we always want a + reliable checksum. The flag does not work: On Unix a + checksum file is created without the flag because it is the + default there. When checking it on Windows the missing flag + would indicate that it has been created in text mode and thus + the checksums will differ. */ + + /* Lowercase the checksum. */ + line[name_offset-2] = 0; + for (p=line; *p; p++) + if (*p >= 'A' && *p <= 'Z') + *p |= 0x20; + /* Unescape the fname. */ + if (escaped) + unescapefname (line+name_offset); + /* Hash the file. */ + if (hash_file (line+name_offset, line)) + rc = -1; + } + + if (es_ferror (fp)) + { + fprintf (stderr, "Error reading `%s': %s\n", + fname, strerror (errno)); + rc = -1; + } + if (fp != stdin) + es_fclose (fp); + + return rc; +} + +static gpg_error_t +hash_list (void) +{ + int rc = 0; + int ready = 0; + int c; + char namebuf[4096]; + size_t n = 0; + unsigned long lastoff = 0; + unsigned long off = 0; + + es_set_binary(es_stdin); + do + { + if ((c = es_getc (es_stdin)) == EOF) + { + if (es_ferror (es_stdin)) + { + log_error ("Error reading '%s' at offset %lu: %s\n", + "[stdin]", off, strerror (errno)); + rc = -1; + break; + } + /* Note: The Nul is a delimiter and not a terminator. */ + c = 0; + ready = 1; + } + if (n >= sizeof namebuf) + { + log_error ("Error reading '%s': " + "filename at offset %lu too long\n", + "[stdin]", lastoff); + rc = -1; + break; + } + namebuf[n++] = c; + off++; + if (!c) + { + if (*namebuf && hash_file (namebuf, NULL)) + rc = -1; + n = 0; + lastoff = off; + } + } + while (!ready); + + return rc; +} + +int +main (int argc, char **argv) +{ + gpgrt_argparse_t pargs; + char *executable = argv[0]; + char *maybe_executable; + char *algo_str; + + int rc = 0; + + gnupg_reopen_std ("gpgsum"); + i18n_init(); + init_common_subsystems(&argc, &argv); + + gpgrt_set_strusage (my_strusage); + pargs.argc = &argc; + pargs.argv = &argv; + pargs.flags = ARGPARSE_FLAG_KEEP; + parse_arguments (&pargs, opts); + gpgrt_argparse (NULL, &pargs, NULL); + +#ifdef _WIN32 + maybe_executable = strrchr(executable, '\\'); +#else + maybe_executable = strrchr(executable, '/'); +#endif + if (maybe_executable) + { + executable = ++maybe_executable; + } + + algo_str = executable; + if (strlen(executable) > 3) + { + algo_str = gpgrt_strdup(executable); + if (strstr(algo_str, ".exe") != NULL) + algo_str[strlen(algo_str) - 7] = 0; + else + algo_str[strlen(algo_str) - 3] = 0; + } + + opt.algo = gcry_md_map_name(algo_str); + + if (!opt.algo) + { + //unknown algo + gpgrt_usage (1); + } + gpgrt_free(algo_str); + if (opt.filenames && opt.check) { + gpgrt_usage (1); + } + + if (opt.filenames) + { + /* With option -0 a dash must be given as filename. */ + if (argc != 1 || strcmp (argv[0], "-")) + gpgrt_usage (1); + if (hash_list ()) + rc = 1; + } + else + { + for (; argc; argv++, argc--) + { + if (opt.check) + { + if (check_file (*argv)) + rc = 1; + } + else + { + if (hash_file (*argv, NULL)) + rc = 1; + } + } + } + + if (opt.check && readerrors) + log_error ("WARNING: %u of %u listed files could not be read\n", + readerrors, filecount); + if (opt.check && matcherrors) + log_error ("WARNING: %u of %u computed checksums did NOT match\n", + matcherrors, checkcount); + + return rc; +} diff --git a/tools/gpgsum.w32-manifest.in b/tools/gpgsum.w32-manifest.in new file mode 100644 index 000000000..4bdedb4a9 --- /dev/null +++ b/tools/gpgsum.w32-manifest.in @@ -0,0 +1,25 @@ + + +GNU Privacy Guard (Hashsum tool) + + + + + + + + + + + + + + + + + +