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.wuer...@fau.de>
Signed-off-by: Phillip Raffeck <phillip.raff...@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

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(&timesig))
+               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(&timesig))
+               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;
+}
-- 
2.8.0.rc0.62.gfc8aefa.dirty

--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majord...@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to