This commit adds a helper tool called `git-timestamp-util`, which does the actual RFC3161 time-stamping work. It depends on libssl and libcrypto.
In particular, it is used for creating time-stamp signatures and for verifying them.
To create a time-stamp signature, a Time Stamping Query (TSQ) is created and passed to the helper tool `git-http-timestamp`, which passes it to a Time Stamping Authority and outputs a trusted Time Stamping Response (TSR). The TSR is then split into the time-stamp signature itself and the Time Stamping Autority's certificate. This certificate is stored in a repository-global TSA store file called .git_tsa_store, whereas the raw time-stamp signature is passed to the caller to be stored in a git object. Splitting the TSR into the TSA's certificate and the raw time-stamp signature is done to avoid redundancy as the TSA's certificate will likely not change over years.
To verify a time-stamp signature, a SHA-1 hash of the git object to be checked is passed along with its corresponding time-stamp signature. Identifying certificate information like issuer and serial number is extracted from the time-stamp signature. The tuple of issuer and serial number is then used to find the actual certificate of the Time Stamping Autority in .git_tsa_store file. The TSA's Certificate and the raw time-stamp signature are merged together and verified.
Signed-off-by: Anton Würfel <anton.wuerfel@fau.de>
Signed-off-by: Phillip Raffeck <phillip.raffeck@fau.de>
---
.gitignore | 1 +
Makefile | 7 +
command-list.txt | 1 +
timestamp-util.c | 615 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 624 insertions(+)
create mode 100644 timestamp-util.c
Show changes to 4 files +624 −0
.gitignore, Makefile, command-list.txt, timestamp-util.c
diff --git a/.gitignore b/.gitignore
index a3b270d..08005ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -160,6 +160,7 @@
/git-svn
/git-symbolic-ref
/git-tag
+/git-timestamp-util
/git-unpack-file
/git-unpack-objects
/git-update-index
diff --git a/Makefile b/Makefile
index c717af7..a0ab96a 100644
--- a/Makefile
+++ b/Makefile
@@ -1142,6 +1142,9 @@ ifndef NO_OPENSSL
ifdef NO_HMAC_CTX_CLEANUP
BASIC_CFLAGS += -DNO_HMAC_CTX_CLEANUP
endif
+
+ PROGRAM_OBJS += timestamp-util.o
+ PROGRAMS += git-timestamp-util$X
else
BASIC_CFLAGS += -DNO_OPENSSL
BLK_SHA1 = 1
@@ -2025,6 +2028,10 @@ git-http-timestamp$X: http.o http-timestamp.o GIT-LDFLAGS $(GITLIBS)
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
$(CURL_LIBCURL) $(LIBS)
+git-timestamp-util$X: timestamp-util.o GIT-LDFLAGS $(GITLIBS)
+ $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
+ $(LIBS)
+
$(REMOTE_CURL_ALIASES): $(REMOTE_CURL_PRIMARY)
$(QUIET_LNCP)$(RM) $@ && \
ln $< $@ 2>/dev/null || \
diff --git a/command-list.txt b/command-list.txt
index 3e279c1..07a4cab 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -136,6 +136,7 @@ git-submodule mainporcelain
git-svn foreignscminterface
git-symbolic-ref plumbingmanipulators
git-tag mainporcelain history
+git-timestamp-util purehelpers
git-unpack-file plumbinginterrogators
git-unpack-objects plumbingmanipulators
git-update-index plumbingmanipulators
diff --git a/timestamp-util.c b/timestamp-util.c
new file mode 100644
index 0000000..fee4fc2
--- /dev/null
+++ b/timestamp-util.c
@@ -0,0 +1,615 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <openssl/bio.h>
+#include <openssl/err.h>
+#include <openssl/pem.h>
+#include <openssl/rand.h>
+#include <openssl/ts.h>
+#include <openssl/bn.h>
+
+#include "cache.h"
+#include "run-command.h"
+#include "strbuf.h"
+
+/*
+ * This code is based on the RFC3161 implementation in OpenSSL
+ * by Zoltan Glozik.
+ */
+
+#define NONCE_LENGTH (64)
+#define ISSUER_LEN (1024)
+
+struct tsr_info {
+ char issuer[ISSUER_LEN];
+ unsigned long serial;
+ X509 *cert;
+};
+
+
+static const char *timesig_cmd = "http-timestamp";
+
+static const char *config_ca_key = "ts.capath";
+static const char *certstore_file = ".git_tsa_store";
+static const char *certstore_header_begin = "-----BEGIN ENTRY-----";
+static const char *certstore_header_end = "-----END ENTRY-----";
+
+static TS_REQ *create_query(const char *digest);
+static ASN1_INTEGER *create_nonce(int bits);
+static TS_MSG_IMPRINT *create_msg_imprint(const char *digest);
+
+static int output_as_base64(TS_RESP *response);
+static TS_RESP *strip_tsr(TS_RESP *response);
+
+static int do_verify(TS_RESP *response, const char *digest, const char *CApath,
+ X509 *untrusted);
+static X509_STORE *create_cert_store(const char *CApath);
+static TS_VERIFY_CTX *create_verify_ctx(const char *digest, const char *CApath,
+ X509 *cert);
+
+static int append_certificate_to_store(struct tsr_info *info);
+static X509 *find_certificate_in_store(struct tsr_info *info);
+
+static int extract_info_from_response(struct tsr_info *info, TS_RESP *response);
+
+static void usage_and_die(const char *name);
+
+static int create_tsr(const char *sha1);
+static int verify_tsr(const char *sha1);
+
+/*
+ * Request a TSR from a Time Stamping Authority. The TSR includes the
+ * authority's public key. To avoid saving this public key for every
+ * time-stamped git object, the public key is stripped from the TSR and saved
+ * separately. By default, it is stored in .git_tsa_store file.
+ */
+static int create_tsr(const char *sha1)
+{
+ TS_RESP *response = NULL;
+ TS_REQ *query = NULL;
+ X509 *cert = NULL;
+ BIO *out_bio = NULL;
+ BIO *in_bio = NULL;
+ struct child_process timesig = CHILD_PROCESS_INIT;
+ struct tsr_info info;
+ int retval = 1;
+ const char *args[] = {
+ timesig_cmd,
+ NULL
+ };
+
+ /*
+ * Invoke git-http-timestamp to receive a TSR from
+ * the Time Stamping Authority.
+ */
+ timesig.argv = args;
+ timesig.in = -1;
+ timesig.out = -1;
+ timesig.git_cmd = 1;
+
+ query = create_query(sha1);
+ if (!query) {
+ ERR_print_errors_fp(stderr);
+ goto end;
+ }
+
+ if (start_command(×ig))
+ return error(_("could not run git-%s"), timesig_cmd);
+
+ out_bio = BIO_new_fd(timesig.in, BIO_NOCLOSE | BIO_FP_TEXT);
+ if (!out_bio)
+ goto end;
+
+ if (!i2d_TS_REQ_bio(out_bio, query))
+ goto end;
+
+ close(timesig.in);
+
+ in_bio = BIO_new_fd(timesig.out, BIO_NOCLOSE);
+ if (!in_bio)
+ goto end;
+
+ response = d2i_TS_RESP_bio(in_bio, NULL);
+ if (!response)
+ goto end;
+
+ close(timesig.out);
+
+ if (finish_command(×ig))
+ goto end;
+
+ extract_info_from_response(&info, response);
+
+ /* Add certificate to TSA store if it does not exist yet */
+ cert = find_certificate_in_store(&info);
+ if (!cert) {
+ retval = append_certificate_to_store(&info);
+ if (retval)
+ goto end;
+ }
+
+ /* Strip certificate from TSR */
+ strip_tsr(response);
+ if (!response)
+ goto end;
+
+ /* Send stripped TSR to stdout */
+ if (output_as_base64(response))
+ goto end;
+
+ retval = 0;
+
+end:
+ BIO_free_all(out_bio);
+ BIO_free_all(in_bio);
+ TS_RESP_free(response);
+ TS_REQ_free(query);
+ X509_free(cert);
+ return retval;
+}
+
+/*
+ * Verify a TSR passed via stdin. The base64 encoded TSR is combined with the
+ * corresponding public key of the Time Stamping Authority (TSA) and then passed
+ * to the `openssl ts -verify` command.
+ */
+static int verify_tsr(const char *sha1)
+{
+ BIO *b64 = NULL;
+ BIO *in = NULL;
+ BIO *out = NULL;
+ TS_RESP *response = NULL;
+ TS_TST_INFO *tst_info = NULL;
+ X509 *cert = NULL;
+ int retval = 1;
+ char *config_ca_path;
+ struct tsr_info info;
+
+ /* get config options */
+ if (git_config_get_pathname(config_ca_key,
+ (const char **)&config_ca_path))
+ die(_("git config option '%s' must be set"), config_ca_key);
+
+ /* prepare BIO-stdout */
+ out = BIO_new_fp(stdout, BIO_NOCLOSE);
+ if (!out)
+ goto end;
+
+ /* read in base64 encoded stripped tsr from stdin */
+ b64 = BIO_new(BIO_f_base64());
+ in = BIO_new_fp(stdin, BIO_NOCLOSE);
+ if (!in)
+ goto end;
+
+ in = BIO_push(b64, in);
+
+ response = d2i_TS_RESP_bio(in, NULL);
+ if (!response)
+ goto end;
+
+ extract_info_from_response(&info, response);
+ cert = find_certificate_in_store(&info);
+ if (!cert) {
+ error(_("certificate not found in %s"), certstore_file);
+ goto end;
+ }
+
+ if (do_verify(response, sha1, config_ca_path, cert)) {
+ error("BAD time-stamp signature");
+ ERR_print_errors(out);
+ goto end;
+ }
+
+ /* prepare data for output */
+ tst_info = TS_RESP_get_tst_info(response);
+
+ BIO_puts(out, "Verified time-stamp: ");
+ ASN1_GENERALIZEDTIME_print(out, tst_info->time);
+ BIO_printf(out, "\nTime Stamping Authority: %s\n",
+ info.issuer);
+
+ retval = 0;
+
+end:
+ TS_RESP_free(response);
+ BIO_free_all(in);
+ BIO_free_all(out);
+ free(config_ca_path);
+
+ return retval;
+}
+
+static int do_verify(TS_RESP *response, const char *digest, const char *CApath,
+ X509 *cert)
+{
+ TS_VERIFY_CTX *verify_ctx = NULL;
+ int ret = 0;
+
+ verify_ctx = create_verify_ctx(digest, CApath, cert);
+ if (!verify_ctx)
+ goto end;
+
+ ret = TS_RESP_verify_response(verify_ctx, response);
+
+end:
+ /*
+ * TS_VERIFY_CTX_free also cleans up the created X509_STORE, so no
+ * further action is needed.
+ */
+ TS_VERIFY_CTX_free(verify_ctx);
+
+ /*
+ * Invert ret to follow git return semantics. 0 indicates success,
+ * anything else indicates errors.
+ */
+ return !ret;
+}
+
+static int append_certificate_to_store(struct tsr_info *info)
+{
+ FILE *store = fopen(certstore_file, "a");
+
+ if (!store) {
+ return error(_("Failed to open the certificate store at "
+ "%s: %s"), certstore_file, strerror(errno));
+ }
+
+ fprintf(store, "%s\nVersion: 1\nSerial: %lu\nIssuer: %s\n\n",
+ certstore_header_begin,
+ info->serial,
+ info->issuer
+ );
+
+ if (!PEM_write_X509(store, info->cert)) {
+ fclose(store);
+ return 1;
+ }
+
+ fprintf(store, "%s\n", certstore_header_end);
+ fclose(store);
+
+ return 0;
+}
+
+static X509 *find_certificate_in_store(struct tsr_info *info)
+{
+ char buf[1024];
+ char issuer[ISSUER_LEN];
+ X509 *cert = NULL;
+ unsigned long serial;
+ FILE *store;
+
+ store = fopen(certstore_file, "r");
+ if (!store) {
+ if (errno == ENOENT)
+ return NULL;
+ die(_("Failed to open the certificate store at "
+ "%s: %s"), certstore_file, strerror(errno));
+ }
+
+ while (fgets(buf, 1024, store)) {
+ if (!starts_with(buf, certstore_header_begin))
+ continue;
+
+ serial = 0;
+ *issuer = 0;
+ /* We are reading in an entry. Read in meta-data.*/
+ while (fgets(buf, 1024, store)) {
+ if (starts_with(buf, "Serial:"))
+ sscanf(buf, "Serial: %lu\n", &serial);
+
+ if (starts_with(buf, "Issuer:"))
+ sscanf(buf, "Issuer: %[^\n]\n", issuer);
+
+ /* A empty line separates meta-data from certificate */
+ if (!strcmp(buf, "\n"))
+ break;
+ if (starts_with(buf, certstore_header_end)) {
+ serial = 0;
+ *issuer = 0;
+ break;
+ }
+ }
+
+ /* Check if certificate matches */
+ if (serial != info->serial ||
+ strcmp(issuer, info->issuer))
+ continue;
+
+ /* Matching certificate found */
+ cert = PEM_read_X509(store, NULL, NULL, NULL);
+
+ if (cert)
+ break;
+
+ /* Certificate could not be read. Output a warning. */
+ fprintf(stderr, "Warning: Failed to read certificate with serial: "
+ "%lu and issuer: %s. "
+ "It may be corrupted.\n", serial, issuer);
+ }
+
+ fclose(store);
+ return cert;
+}
+
+/* Extract issuer, serial number and TSA's public key from a TS_RESP struct */
+static int extract_info_from_response(struct tsr_info *info, TS_RESP *response)
+{
+ PKCS7 *token;
+ PKCS7_SIGNER_INFO *pksi = NULL;
+ PKCS7_ISSUER_AND_SERIAL *pkis = NULL;
+
+ token = response->token;
+ pksi = sk_PKCS7_SIGNER_INFO_value(token->d.sign->signer_info, 0);
+ pkis = pksi->issuer_and_serial;
+
+ X509_NAME_get_text_by_NID(pkis->issuer, NID_commonName, info->issuer,
+ ISSUER_LEN);
+ info->serial = ASN1_INTEGER_get(pkis->serial);
+ info->cert = sk_X509_value(token->d.sign->cert, 0);
+
+ return 0;
+}
+
+/* Use libssl functions to convert data to base64 and output it via stdout */
+static int output_as_base64(TS_RESP *response)
+{
+ BIO *b64 = NULL;
+ BIO *out = NULL;
+
+ b64 = BIO_new(BIO_f_base64());
+ out = BIO_new_fp(stdout, BIO_NOCLOSE);
+ out = BIO_push(b64, out);
+
+ if (!i2d_TS_RESP_bio(out, response))
+ return 1;
+
+ BIO_flush(out);
+ BIO_free_all(out);
+
+ return 0;
+}
+
+/*
+ * Strip TSR by removing all X509 certificates as they are stored in a dedicated
+ * TSA certificate store.
+ */
+static TS_RESP *strip_tsr(TS_RESP *response)
+{
+ while (sk_X509_num(response->token->d.sign->cert) > 0)
+ sk_X509_pop(response->token->d.sign->cert);
+
+ return response;
+}
+
+static TS_REQ *create_query(const char *digest)
+{
+ int fail = 0;
+ TS_REQ *ts_req = NULL;
+ TS_MSG_IMPRINT *msg_imprint = NULL;
+ ASN1_INTEGER *nonce = NULL;
+
+ /* Creating request object. */
+ ts_req = TS_REQ_new();
+ if (!ts_req)
+ goto err;
+
+ /* Setting version. */
+ if (!TS_REQ_set_version(ts_req, 1))
+ goto err;
+
+ /* Setting MSG_IMPRINT */
+ msg_imprint = create_msg_imprint(digest);
+ if (!msg_imprint)
+ goto err;
+ if (!TS_REQ_set_msg_imprint(ts_req, msg_imprint))
+ goto err;
+
+ /* Setting nonce. */
+ nonce = create_nonce(NONCE_LENGTH);
+ if (!nonce)
+ goto err;
+ if (!TS_REQ_set_nonce(ts_req, nonce))
+ goto err;
+
+ /* Set certificate request flag. */
+ if (!TS_REQ_set_cert_req(ts_req, 1))
+ goto err;
+
+ fail = 1;
+err:
+ if (!fail) {
+ TS_REQ_free(ts_req);
+ ts_req = NULL;
+ }
+
+ TS_MSG_IMPRINT_free(msg_imprint);
+ ASN1_INTEGER_free(nonce);
+ return ts_req;
+}
+
+static TS_MSG_IMPRINT *create_msg_imprint(const char *digest)
+{
+ long len;
+ const EVP_MD *md = NULL;
+ TS_MSG_IMPRINT *msg_imprint = NULL;
+ X509_ALGOR *algo = NULL;
+ unsigned char *data = NULL;
+ int fail = 1;
+
+ /* Creating MSG_IMPRINT object. */
+ msg_imprint = TS_MSG_IMPRINT_new();
+ if (!msg_imprint)
+ goto err;
+
+ data = string_to_hex(digest, &len);
+ if (!TS_MSG_IMPRINT_set_msg(msg_imprint, data, len))
+ goto err;
+
+ /* Adding sha1 algorithm. */
+ md = EVP_sha1();
+ algo = X509_ALGOR_new();
+ if (!algo)
+ goto err;
+ algo->algorithm = OBJ_nid2obj(EVP_MD_type(md));
+ if (!algo->algorithm)
+ goto err;
+ algo->parameter = ASN1_TYPE_new();
+ if (!algo->parameter)
+ goto err;
+ algo->parameter->type = V_ASN1_NULL;
+ if (!TS_MSG_IMPRINT_set_algo(msg_imprint, algo))
+ goto err;
+
+ fail = 0;
+
+err:
+ if (fail) {
+ TS_MSG_IMPRINT_free(msg_imprint);
+ msg_imprint = NULL;
+ }
+
+ OPENSSL_free(data);
+ X509_ALGOR_free(algo);
+ return msg_imprint;
+}
+
+static ASN1_INTEGER *create_nonce(int bits)
+{
+ unsigned char buf[20];
+ ASN1_INTEGER *nonce = NULL;
+ int len = (bits - 1) / 8 + 1;
+ int i;
+
+ /* Generating random byte sequence. */
+ if (len > (int) sizeof(buf))
+ goto err;
+ if (RAND_bytes(buf, len) <= 0)
+ goto err;
+
+ /* Find the first non-zero byte and creating ASN1_INTEGER object. */
+ for (i = 0; i < len && !buf[i]; ++i)
+ ;
+ nonce = ASN1_INTEGER_new();
+ if (!nonce)
+ goto err;
+ OPENSSL_free(nonce->data);
+
+ /* Allocate at least one byte. */
+ nonce->length = len - i;
+ nonce->data = OPENSSL_malloc(nonce->length + 1);
+ if (!nonce->data)
+ goto err;
+ memcpy(nonce->data, buf + i, nonce->length);
+
+ return nonce;
+ err:
+ ASN1_INTEGER_free(nonce);
+ return NULL;
+}
+
+static TS_VERIFY_CTX *create_verify_ctx(const char *digest, const char *CApath,
+ X509 *cert)
+{
+ TS_VERIFY_CTX *ctx = NULL;
+ unsigned char *sha1 = NULL;
+ long imprint_len;
+ int f = 0;
+
+ ctx = TS_VERIFY_CTX_new();
+ if (!ctx)
+ goto err;
+
+ f = TS_VFY_VERSION | TS_VFY_SIGNER | TS_VFY_IMPRINT | TS_VFY_SIGNATURE;
+ sha1 = string_to_hex(digest, &imprint_len);
+ if (!sha1)
+ goto err;
+
+ /* Add digest to verification context. */
+ ctx->imprint = sha1;
+ ctx->imprint_len = imprint_len;
+
+ /* Add the signature verification flag and arguments. */
+ ctx->flags = f;
+
+ /* Initialising the X509_STORE object. */
+ ctx->store = create_cert_store(CApath);
+ if (!ctx->store)
+ goto err;
+
+ /* Add signing certificate of TSA */
+ ctx->certs = sk_X509_new_null();
+ if (!ctx->certs)
+ goto err;
+
+ sk_X509_push(ctx->certs, cert);
+
+
+ return ctx;
+
+err:
+ /*
+ * TS_VERIFY_CTX also cleans up *sha1 and the created X509 store, so no
+ * further action is needed.
+ */
+ TS_VERIFY_CTX_free(ctx);
+ return NULL;
+}
+
+static X509_STORE *create_cert_store(const char *CApath)
+{
+ X509_STORE *cert_ctx = NULL;
+ X509_LOOKUP *lookup = NULL;
+ int ret;
+
+ cert_ctx = X509_STORE_new();
+ lookup = X509_STORE_add_lookup(cert_ctx, X509_LOOKUP_hash_dir());
+ if (!lookup)
+ goto err;
+
+ ret = X509_LOOKUP_add_dir(lookup, CApath, X509_FILETYPE_PEM);
+ if (!ret) {
+ error(_("Error loading directory %s"), CApath);
+ goto err;
+ }
+
+ return cert_ctx;
+
+err:
+ X509_STORE_free(cert_ctx);
+ return NULL;
+}
+
+
+static void usage_and_die(const char *name)
+{
+ fprintf(stderr, "Usage: %s <-c | -v> [<sha1>]\n\n", name);
+ fputs("-c: Create a time-stamp signature for the given SHA1-hash\n",
+ stderr);
+ fputs("-v: Verify a time-stamp signature from data passed via stdin\n",
+ stderr);
+
+ exit(EXIT_FAILURE);
+}
+
+int main(int argc, char *argv[])
+{
+ CRYPTO_malloc_init();
+ ERR_load_crypto_strings();
+ OpenSSL_add_all_algorithms();
+
+ if (argc != 3)
+ usage_and_die(argv[0]);
+
+ if (!strcmp(argv[1], "-c"))
+ return create_tsr(argv[2]);
+
+ else if (!strcmp(argv[1], "-v"))
+ return verify_tsr(argv[2]);
+
+ else
+ usage_and_die(argv[0]);
+
+ return 0;
+}