diff --git a/dirmngr/Makefile.am b/dirmngr/Makefile.am index a0f8d5a79..4b5b8881e 100644 --- a/dirmngr/Makefile.am +++ b/dirmngr/Makefile.am @@ -86,7 +86,8 @@ dirmngr_SOURCES = dirmngr.c dirmngr.h server.c crlcache.c crlfetch.c \ dns-stuff.c dns-stuff.h \ http.c http.h http-common.c http-common.h http-ntbtls.c \ ks-action.c ks-action.h ks-engine.h \ - ks-engine-hkp.c ks-engine-http.c ks-engine-finger.c ks-engine-kdns.c + ks-engine-hkp.c ks-engine-http.c ks-engine-finger.c ks-engine-kdns.c \ + rfc3161.c if USE_LIBDNS dirmngr_SOURCES += dns.c dns.h diff --git a/dirmngr/dirmngr.c b/dirmngr/dirmngr.c index f79a0f877..0ac1a7307 100644 --- a/dirmngr/dirmngr.c +++ b/dirmngr/dirmngr.c @@ -161,6 +161,7 @@ enum cmd_and_opt_values { oListenBacklog, oFakeCRL, oCompatibilityFlags, + oTSAResponder, aTest }; @@ -308,6 +309,8 @@ static gpgrt_opt_t opts[] = { ARGPARSE_s_n (oBatch, "batch", "@"), ARGPARSE_s_s (oHTTPWrapperProgram, "http-wrapper-program", "@"), + ARGPARSE_s_s (oTSAResponder, "tsa-responder", + N_("|URL|use TSA responder at URL")), ARGPARSE_group (302,N_("@\n(See the \"info\" manual for a complete listing " "of all commands and options)\n")), @@ -726,6 +729,7 @@ parse_rereadable_options (gpgrt_argparse_t *pargs, int reread) xfree (opt.fake_crl); opt.fake_crl = NULL; opt.compat_flags = 0; + opt.tsa_responder = NULL; return 1; } @@ -905,6 +909,7 @@ parse_rereadable_options (gpgrt_argparse_t *pargs, int reread) pargs->err = ARGPARSE_PRINT_WARNING; } break; + case oTSAResponder: opt.tsa_responder = pargs->r.ret_str; break; default: return 0; /* Not handled. */ diff --git a/dirmngr/dirmngr.h b/dirmngr/dirmngr.h index 50c97f140..e1fae6574 100644 --- a/dirmngr/dirmngr.h +++ b/dirmngr/dirmngr.h @@ -162,6 +162,7 @@ struct /* Compatibility flags (COMPAT_FLAG_xxxx). */ unsigned int compat_flags; + char *tsa_responder; } opt; diff --git a/dirmngr/rfc3161.c b/dirmngr/rfc3161.c new file mode 100644 index 000000000..eb514898a --- /dev/null +++ b/dirmngr/rfc3161.c @@ -0,0 +1,522 @@ +/* rfc3161.c - X.509 Time-Stamp protocol using HTTPS transport. + * Copyright (C) 2022-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 "dirmngr.h" +#include "misc.h" +#include "http.h" +#include "validate.h" +#include "certcache.h" +#include "rfc3161.h" +#include "../common/tlv.h" +#include "../common/exechelp.h" + +#include + +/* The maximum size we allow as a response from TSA. */ +#define MAX_RESPONSE_SIZE 65536 + +/* Read from FP and return a newly allocated buffer in R_BUFFER with the + entire data read from FP. */ +static gpg_error_t +read_response (estream_t fp, unsigned char **r_buffer, size_t *r_buflen) +{ + gpg_error_t err; + unsigned char *buffer; + size_t bufsize, nbytes; + + *r_buffer = NULL; + *r_buflen = 0; + + bufsize = 4096; + buffer = xtrymalloc (bufsize); + if (!buffer) + return gpg_error_from_errno (errno); + + nbytes = 0; + for (;;) + { + unsigned char *tmp; + size_t nread = 0; + + assert (nbytes < bufsize); + nread = es_fread (buffer+nbytes, 1, bufsize-nbytes, fp); + if (nread < bufsize-nbytes && es_ferror (fp)) + { + err = gpg_error_from_errno (errno); + log_error (_("error reading from responder: %s\n"), + strerror (errno)); + xfree (buffer); + return err; + } + if ( !(nread == bufsize-nbytes && !es_feof (fp))) + { /* Response successfully received. */ + nbytes += nread; + *r_buffer = buffer; + *r_buflen = nbytes; + return 0; + } + + nbytes += nread; + + /* Need to enlarge the buffer. */ + if (bufsize >= MAX_RESPONSE_SIZE) + { + log_error (_("response from server too large; limit is %d bytes\n"), + MAX_RESPONSE_SIZE); + xfree (buffer); + return gpg_error (GPG_ERR_TOO_LARGE); + } + + bufsize += 4096; + tmp = xtryrealloc (buffer, bufsize); + if (!tmp) + { + err = gpg_error_from_errno (errno); + xfree (buffer); + return err; + } + buffer = tmp; + } +} + +static gpg_error_t +tsa_parse_response (const unsigned char *buffer, size_t length, + ksba_cms_t *r_cms, unsigned char **r_signed_data, + size_t *r_signed_data_length) +{ + gpg_error_t err = 0; + const char *where = ""; + tlv_parser_t tlv; + gcry_md_hd_t hd; + unsigned char *tmperror; + unsigned char *error; + size_t error_length; + const unsigned char *failinfo; + size_t failinfo_length; + const unsigned char *tmp_signed; + struct tag_info info; + size_t len; + int status; + ksba_reader_t reader; + ksba_stop_reason_t stopreason; + const char *algoid; + int algo; + int i; + + tlv = tlv_parser_new (buffer, length, 0); + if (!tlv) + { + err = gpg_error_from_syserror(); + goto bailout; + } + where = "start"; + if (tlv_next(tlv)) + goto bailout; + if (tlv_expect_sequence(tlv)) + goto bailout; + + where = "status"; + if (tlv_next(tlv)) + goto bailout; + if (tlv_expect_sequence(tlv)) + goto bailout; + + where = "pkistatus"; + if (tlv_next(tlv)) + goto bailout; + + if (tlv_expect_integer(tlv, &status)) + goto bailout; + + if (status != 0) { + if (tlv_next(tlv)) + goto bailout; + where = "statusString"; + if (tlv_expect_sequence(tlv)) + goto bailout; + + where = "failInfo"; + if (tlv_next(tlv)) + goto bailout; + + errors: + if (tlv_expect_object(tlv, CLASS_UNIVERSAL, TAG_UTF8_STRING, &tmperror, &error_length)) { + goto bailout; + } + error = xtrymalloc(error_length + 1); + memcpy(error, tmperror, error_length); + error[error_length] = 0; + log_error("Error: %s\n", error); + xfree(error); + + if (tlv_next(tlv)) + goto bailout; + if (tlv_parser_level(tlv) == 3) + goto errors; + + if (tlv_expect_object(tlv, CLASS_UNIVERSAL, TAG_BIT_STRING, &failinfo, &failinfo_length)) + goto bailout; + return GPG_ERR_SERVER_FAILED; + } + + *r_signed_data = buffer + tlv_parser_offset(tlv); + tmp_signed = *r_signed_data; + len = length - tlv_parser_offset(tlv); + err = tlv_parse_tag(r_signed_data, &len, &info); + if (err) + goto bailout; + *r_signed_data_length = info.length + info.nhdr; + *r_signed_data = xmalloc(*r_signed_data_length); + memcpy(*r_signed_data, tmp_signed, *r_signed_data_length); + tlv_parser_release(tlv); + + ksba_reader_new(&reader); + ksba_reader_set_mem(reader, *r_signed_data, *r_signed_data_length); + + ksba_cms_set_reader_writer(*r_cms, reader, NULL); + + where = "parse_cert"; + err = gcry_md_open(&hd, 0, 0); + if (err) { + return err; + } + do + { + err = ksba_cms_parse(*r_cms, &stopreason); + if (stopreason == KSBA_SR_NEED_HASH + || stopreason == KSBA_SR_BEGIN_DATA) + { + /* We are now able to enable the hash algorithms */ + for (i=0; (algoid=ksba_cms_get_digest_algo_list (*r_cms, i)); i++) + { + algo = gcry_md_map_name (algoid); + if (!algo) + { + log_error ("unknown hash algorithm '%s'\n", + algoid? algoid:"?"); + } + else + { + gcry_md_enable (hd, algo); + } + } + ksba_cms_set_hash_function (*r_cms, HASH_FNC, hd); + } + if (err) + { + log_error("ksba_cms_parse failed: %s\n", gpg_strerror(err)); + goto bailout; + } + } + while (stopreason != KSBA_SR_READY); + gcry_md_close(hd); + if (err) + goto bailout; + + ksba_cms_set_reader_writer(*r_cms, NULL, NULL); + ksba_reader_release(reader); + + return 0; + + bailout: + if (!err) + err = gpg_error (GPG_ERR_GENERAL); + log_error ("%s(%s): @%04zu lvl=%u %s: %s - %s\n", + __func__, where, + tlv_parser_offset (tlv), + tlv_parser_level (tlv), + tlv_parser_lastfunc (tlv), + tlv_parser_lasterrstr (tlv), + gpg_strerror (err)); + tlv_parser_release (tlv); + return err; +} + + +/* Construct an TSP request, send it to the TSA at URL and parse + * the response. */ +static gpg_error_t +do_tsp_request (ctrl_t ctrl, const char *url, char *hashalgooid, + const void *tbshash, unsigned int tbshashlen, + ksba_cms_t *r_cms, unsigned char **r_signed_data, + size_t *r_signed_data_length) +{ + gpg_error_t err; + ksba_der_t dbld = NULL; + + unsigned char *response; + size_t responselen; + http_t http; + int redirects_left = 2; + char *free_this = NULL; + unsigned char *tmpder; + size_t tmpderlen; + + dbld = ksba_der_builder_new (0); + if (!dbld) + { + err = gpg_error_from_syserror (); + goto leave; + } + ksba_der_add_tag (dbld, 0, KSBA_TYPE_SEQUENCE); + ksba_der_add_int (dbld, "\x01", 1, 0); + ksba_der_add_tag (dbld, 0, KSBA_TYPE_SEQUENCE); + ksba_der_add_tag ( dbld, 0, KSBA_TYPE_SEQUENCE); + ksba_der_add_oid ( dbld, hashalgooid); + ksba_der_add_end ( dbld); + ksba_der_add_val ( dbld, 0, KSBA_TYPE_OCTET_STRING, tbshash, tbshashlen); + ksba_der_add_end (dbld); + /* reqPolicy would go here. */ + { + unsigned char nonce[32]; + gcry_create_nonce (nonce, sizeof(nonce)); + ksba_der_add_int (dbld, nonce, sizeof(nonce), 1); + }; + /* Whether we're requesting the certificate */ + //int true_val = 1; + //ksba_der_add_val(dbld, 0, KSBA_TYPE_BOOLEAN, &true_val, 1); + /* certReq would go here. */ + /* extensions would go here. */ + ksba_der_add_end (dbld); + + err = ksba_der_builder_get (dbld, &tmpder, &tmpderlen); + ksba_der_builder_reset(dbld); // TODO is this needed? + if (err) { + goto leave; + } + + once_more: + err = http_open (ctrl, &http, HTTP_REQ_POST, url, NULL, NULL, + ((opt.honor_http_proxy? HTTP_FLAG_TRY_PROXY:0) + | (dirmngr_use_tor ()? HTTP_FLAG_FORCE_TOR:0) + | (opt.disable_ipv4? HTTP_FLAG_IGNORE_IPv4 : 0) + | (opt.disable_ipv6? HTTP_FLAG_IGNORE_IPv6 : 0)), + ctrl->http_proxy, NULL, NULL, NULL); + if (err) + { + log_error (_("error connecting to '%s': %s\n"), url, gpg_strerror (err)); + xfree (free_this); + return err; + } + + es_fprintf (http_get_write_ptr (http), + "Content-Type: application/timestamp-query\r\n" + "Content-Length: %lu\r\n", + (unsigned long)tmpderlen ); + http_start_data (http); + if (es_fwrite (tmpder, tmpderlen, 1, http_get_write_ptr (http)) != 1) + { + err = gpg_error_from_errno (errno); + log_error ("error sending request to '%s': %s\n", url, strerror (errno)); + http_close (http, 0); + xfree (tmpder); + xfree (free_this); + return err; + } + xfree (tmpder); + tmpder = NULL; + + err = http_wait_response (http); + if (err || http_get_status_code (http) != 200) + { + if (err) + log_error (_("error reading HTTP response for '%s': %s\n"), + url, gpg_strerror (err)); + else + { + switch (http_get_status_code (http)) + { + case 301: + case 302: + { + const char *s = http_get_header (http, "Location", 0); + + log_info (_("URL '%s' redirected to '%s' (%u)\n"), + url, s?s:"[none]", http_get_status_code (http)); + if (s && *s && redirects_left-- ) + { + xfree (free_this); url = NULL; + free_this = xtrystrdup (s); + if (!free_this) + err = gpg_error_from_errno (errno); + else + { + url = free_this; + http_close (http, 0); + goto once_more; + } + } + else + err = gpg_error (GPG_ERR_NO_DATA); + log_error (_("too many redirections\n")); + } + break; + + case 413: /* Payload too large */ + err = gpg_error (GPG_ERR_TOO_LARGE); + break; + + default: + log_error (_("error accessing '%s': http status %u\n"), + url, http_get_status_code (http)); + err = gpg_error (GPG_ERR_NO_DATA); + break; + } + } + http_close (http, 0); + xfree (free_this); + return err; + } + + err = read_response (http_get_read_ptr (http), &response, &responselen); + http_close (http, 0); + if (err) + { + log_error (_("error reading HTTP response for '%s': %s\n"), + url, gpg_strerror (err)); + goto leave; + } + + err = tsa_parse_response (response, responselen, r_cms, r_signed_data, + r_signed_data_length); + + if (err) + { + log_error (_("error parsing TSA response for '%s': %s\n"), + url, gpg_strerror (err)); + goto leave; + } + + leave: + xfree (response); + xfree (free_this); + return err; +} + +/* Send a timestamp request to the current TSA (from CTRL) and return + * the answer. HASHALGO shall be provided by the caller; we do no + * consistency checking here. */ +gpg_error_t +dirmngr_get_timestamp (ctrl_t ctrl, char *hashalgoid, + const void *tbshash, unsigned int tbshashlen, ksba_cms_t *r_cms) +{ + gpg_error_t err; + const char *url; + unsigned char *signed_data = NULL; + size_t signed_data_length; + gnupg_isotime_t signing_time; + gnupg_isotime_t current_time; + gnupg_isotime_t tmp_time; + + int exitcode; + estream_t in; + pid_t pid; + + const char *argv[] = { + "--verify", + NULL + }; + + ksba_cms_new(r_cms); + + if (opt.disable_http) + { + log_error (_("Timestamp request not possible due to disabled HTTP\n")); + err = gpg_error (GPG_ERR_NOT_SUPPORTED); + goto leave; + } + + else if (opt.tsa_responder && *opt.tsa_responder) + url = opt.tsa_responder; + else + { + log_info (_("no default URL for a TSA available\n")); + err = gpg_error (GPG_ERR_CONFIGURATION); + goto leave; + } + + /* Ask the TSA. */ + err = do_tsp_request (ctrl, url, hashalgoid, tbshash, tbshashlen, r_cms, + &signed_data, &signed_data_length); + if (err) + goto leave; + /* Allow for some clock skew. */ + gnupg_get_isotime (current_time); + add_seconds_to_isotime (current_time, opt.ocsp_max_clock_skew); + + ksba_cms_get_signing_time (*r_cms, 0, signing_time); + if (strcmp (signing_time, current_time) > 0 ) + { + log_error (_("TSA responder returned a status in the future\n")); + log_info ("used now: %s signing_time: %s\n", current_time, signing_time); + if (!err) + err = gpg_error (GPG_ERR_TIME_CONFLICT); + goto leave; + } + + /* Check that THIS_UPDATE is not too far back in the past. */ + gnupg_copy_time (tmp_time, signing_time); + add_seconds_to_isotime (tmp_time, + 60 + opt.ocsp_max_clock_skew); //TODO configurable + + if (!*tmp_time || strcmp (tmp_time, current_time) < 0 ) + { + log_error (_("TSA responder returned a non-current status\n")); + log_info ("used now: %s signing_time: %s\n", + current_time, signing_time); + if (!err) + err = gpg_error (GPG_ERR_TIME_CONFLICT); + goto leave; + } + + err = gnupg_spawn_process (gnupg_module_name(GNUPG_MODULE_NAME_GPGSM), argv, NULL, 0, &in, NULL, NULL, &pid); + if (err) + goto leave; + + es_fwrite(signed_data, 1, signed_data_length, in); + es_fclose(in); + + gnupg_wait_process(gnupg_module_name(GNUPG_MODULE_NAME_GPGSM), pid, 1, &exitcode); + gnupg_release_process(pid); + if (!exitcode) { + log_error("Signature verification successful\n"); + } else { + log_error("Signature verification not successful\n"); + err = GPG_ERR_BAD_SIGNATURE; + goto leave; + } + + leave: + if (err) + { + ksba_cms_release(*r_cms); + *r_cms = NULL; + goto leave; + } + xfree(signed_data); + return err; +} diff --git a/dirmngr/rfc3161.h b/dirmngr/rfc3161.h new file mode 100644 index 000000000..d9610110c --- /dev/null +++ b/dirmngr/rfc3161.h @@ -0,0 +1,32 @@ +/* rfc3161.h - X.509 Time-Stamp protocol interface + * Copyright (C) 2022 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 + */ + +#ifndef GNUPG_RFC3161_H +#define GNUPG_RFC3161_H + +#include +#include "dirmngr.h" + +gpg_error_t dirmngr_get_timestamp (ctrl_t ctrl, char *hashalgoid, + const void *tbshash, + unsigned int tbshashlen, ksba_cms_t *r_cms); + + +#endif /*GNUPG_RFC3161_H*/ diff --git a/dirmngr/server.c b/dirmngr/server.c index 1dbc87878..db77116b1 100644 --- a/dirmngr/server.c +++ b/dirmngr/server.c @@ -64,6 +64,7 @@ #include "../common/mbox-util.h" #include "../common/zb32.h" #include "../common/server-help.h" +#include "rfc3161.h" /* To avoid DoS attacks we limit the size of a certificate to something reasonable. The DoS was actually only an issue back when @@ -655,6 +656,29 @@ option_handler (assuan_context_t ctx, const char *key, const char *value) return err; } +static gpg_error_t +cmd_tsa (assuan_context_t ctx, char *line) +{ + gpg_error_t err = 0; + unsigned char *digest; + ksba_cms_t cms; + ctrl_t ctrl = assuan_get_pointer(ctx); + gcry_md_hd_t hd; + const char *oid = "2.16.840.1.101.3.4.2.1"; + gcry_md_open(&hd, gcry_md_map_name(oid), 0); + gcry_md_write(hd, line, strlen(line)); + digest = gcry_md_read(hd, 0); + err = dirmngr_get_timestamp(ctrl, oid, digest, 32, &cms); + if (err) + goto leave; + gnupg_isotime_t time; + ksba_cms_get_signing_time(cms, 0, &time); + ksba_cms_release(cms); +leave: + gcry_md_close(hd); + return leave_cmd (ctx, 0); +} + static const char hlp_dns_cert[] = @@ -3049,6 +3073,7 @@ register_commands (assuan_context_t ctx) assuan_handler_t handler; const char * const help; } table[] = { + { "TSA", cmd_tsa, hlp_dns_cert }, { "DNS_CERT", cmd_dns_cert, hlp_dns_cert }, { "WKD_GET", cmd_wkd_get, hlp_wkd_get }, { "LDAPSERVER", cmd_ldapserver, hlp_ldapserver },