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@xxxxxx> Signed-off-by: Phillip Raffeck <phillip.raffeck@xxxxxx> --- .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(×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; +} -- 2.8.0.rc0.62.gfc8aefa.dirty -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html