#!/bin/bash # # RFC3161 and RFC5816 Timestamping for git repositories. # # Copyright (c) 2021 Mabulous GmbH # Authors: Matthias Bühlmann # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # The interactive user interfaces in modified source and object code versions # of this program must display Appropriate Legal Notices, as required under # Section 5 of the GNU Affero General Public License version 3. In accordance # with Section 7(b) of the GNU Affero General Public License, you must retain # the Info line in every timestamp that is created or manipulated using a # covered work. # # You can be released from the requirements of the license by purchasing # a commercial license. Buying such a license is mandatory as soon as you # develop commercial activities involving this software without # disclosing the source code of your own applications. # These activities include: offering paid services to customers as an ASP, # providing data storage and archival services, shipping this software with a # closed source product. # # For more information, please contact Mabulous GmbH at this # address: info@mabulous.com # DIR="${BASH_SOURCE%/*}" if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi . "$DIR/timestamping" TSA0_URL=$(git config timestamping.tsa0.url) if [ -z "$TSA0_URL" ]; then # Do nothing if TSA0 has not been configured. echo_info "Info: No timestamping TSA has been configured." exit 0 fi extended_exit_trap() { local EXIT_CODE="$?" log "extended_exit_trap $EXIT_CODE" if [ $EXIT_CODE -gt 0 ]; then echo_error "Aborting commit." git reset --soft HEAD^ fi rm -rf -- "$TMP_DIR" exit "$EXIT_CODE" } trap "extended_exit_trap" EXIT if [ ! -d "$LTV_DIR"/certs ]; then mkdir -p "$LTV_DIR"/certs fi if [ ! -d "$LTV_DIR"/crls ]; then mkdir -p "$LTV_DIR"/crls fi COMMIT_MSG=$(git show --pretty=format:"%B" --no-patch HEAD) #avoid recursion and validate timestamp if [[ "$COMMIT_MSG" == "$SUBJECT_LINE"* ]]; then log "exiting recursion" exit 0 fi echo_info "Adding Timestamp commit. This may take a moment..." #To arbitrarily extend the validatability of the timestamps in the repository even beyond the time when CAs cease to provide #revocation status, each timestamp commit ensures to contain CRL data of the timestamps in the previous timestamp commit. #In doing so, the lifetime of the timestamps can be arbitrarily extended into the future, so long as there is still at least one #newer timestamp in the repository which is still validatable (TSA CAs are often obliged to provide revocation status for many #years after the expiration of the signature certificate. Thus, so long as a new commit is added within this period, all older #timestamps' lifetime will get extended to the lifetime of the new timestamp). # recursive function to walk the commit history (including splits of merge commits) and downloading CRL data for the first # timestamp commit found in every branch of the history # param1: hash of parent commit from where to start the search (including this commit) retrieve_crl_for_most_recent_parent_timestamps() { local COMMIT_HASH="$1" log "retrieve_crl_for_most_recent_parent_timestamps for $COMMIT_HASH" local TIMESTAMP_COMMIT_VERSION local URL_ARRAY local TOKEN_ARRAY if ! extract_token_from_commit "$COMMIT_HASH" "$TMP_DIR" TIMESTAMP_COMMIT_VERSION URL_ARRAY TOKEN_ARRAY; then return 1 fi local NUM_EXTRACTED="${#TOKEN_ARRAY[@]}" if [ $NUM_EXTRACTED -eq 0 ]; then #this is not a timestamp commit, proceed with parent(s) local PARENTS=$(git cat-file -p "$COMMIT_HASH" | awk '/^$/{exit} /parent/ {print}' | sed 's/parent //') local RETURN_VAL=0 if [ ! -z "$PARENTS" ]; then local PARENT_HASH while read PARENT_HASH; do if ! retrieve_crl_for_most_recent_parent_timestamps "$PARENT_HASH"; then RETURN_VAL=1 fi done <<< $(printf "%s" "$PARENTS") fi return "$RETURN_VAL" fi #iterate over extracted token and download CRL data for ((i=0; i<"$NUM_EXTRACTED"; i++)); do local TOKEN_FILE="${TOKEN_ARRAY[$i]}" local TSA_URL="${URL_ARRAY[$i]}" local DIGEST get_token_digest "$TOKEN_FILE" DIGEST local SIGNING_CERT_ID get_tsa_cert_id "$TOKEN_FILE" SIGNING_CERT_ID #get certificate chain of this token from LTV data local CERT_CHAIN_FILE="$LTV_DIR"/certs/"$SIGNING_CERT_ID".cer if [ ! -s "$CERT_CHAIN_FILE" ]; then #If LTV data is not in the working directory, check it out from the corresponding commit local TMP_CERT_CHAIN_FILE="$TMP_DIR"/"$SIGNING_CERT_ID".cer local PATH_SPEC=$(realpath --relative-to="$ROOT_DIR" "$CERT_CHAIN_FILE") local CERT_CHAIN_CONTENT=$(git show "$COMMIT_HASH":"$PATH_SPEC") && printf "%s" "$CERT_CHAIN_CONTENT" > "$TMP_CERT_CHAIN_FILE" CERT_CHAIN_FILE="$TMP_CERT_CHAIN_FILE" fi if [ ! -s "$CERT_CHAIN_FILE" ]; then CERT_CHAIN_FILE="$TMP_LTV_DIR"/certs/"$SIGNING_CERT_ID".cer build_certificate_chain_for_token "$TOKEN_FILE" "$DIGEST" "$TSA_URL" "$CERT_CHAIN_FILE" fi assert "[ -s $CERT_CHAIN_FILE ]" "Certificate chain could neither be extracted from LTV data nor reconstructed." #download CRL file local CRL_CHAIN_FILE="$TMP_LTV_DIR"/crls/"$SIGNING_CERT_ID".crl if ! download_crls_for_chain "$CERT_CHAIN_FILE" "$CRL_CHAIN_FILE"; then return 1 fi done return 0 } #get hash of unstamped commit PEARENT_COMMIT_HASH=$(git rev-parse --verify HEAD) if ! retrieve_crl_for_most_recent_parent_timestamps "$PEARENT_COMMIT_HASH"; then echo_error "ERROR: Failed to download CRLs for last timestamp commit." exit 1 fi #In most cases the LTV data downloaded at this point won't change. However, if a new TSA #is being used or if a TSA changed its signing certificate since the previous timestamp commit, #additional LTV data needs to be added to the index, which will change the DIGEST that is signed. #Thus, retrieving the tokens must sometimes be repeated once. DIGEST_TO_TIMESTAMP='' for ((i=0;; i++)); do #add all ltv files if ls "$TMP_LTV_DIR"/*/* &> "$OUT_STREAM"; then ls "$TMP_LTV_DIR"/*/* | while read SOURCE_FILE; do TARGET_FILE="$LTV_DIR"${SOURCE_FILE#"$TMP_LTV_DIR"} cp -f "$SOURCE_FILE" "$TARGET_FILE" git add "$TARGET_FILE" done fi TREE_HASH=$(git write-tree) declare PREIMAGE get_preimage_from_tree_and_parent "$TREE_HASH" "$PEARENT_COMMIT_HASH" PREIMAGE declare NEW_DIGEST_TO_TIMESTAMP compute_digest_from_tree_and_parent "$TREE_HASH" "$PEARENT_COMMIT_HASH" NEW_DIGEST_TO_TIMESTAMP if [ "$NEW_DIGEST_TO_TIMESTAMP" == "$DIGEST_TO_TIMESTAMP" ];then #no new LTV data added, no need to re-request token. break fi if [ $i -gt 0 ]; then echo_info "New LTV data has been added, need to request token again." fi DIGEST_TO_TIMESTAMP="$NEW_DIGEST_TO_TIMESTAMP" #prepare commit message COMMIT_MSG_FILE="$TMP_DIR"/commit_msg.txt printf '%s\n' "$SUBJECT_LINE" > "$COMMIT_MSG_FILE" printf '\n%s %s\n' "$TRAILER_TOKEN_VERSION" "$TIMESTAMPING_VERSION" >> "$COMMIT_MSG_FILE" printf '\n%s %s\n' "$TRAILER_TOKEN_ALGO" "$ALGO" >> "$COMMIT_MSG_FILE" printf '\n%s %s\n' "$TRAILER_TOKEN_PREIMAGE" "$PREIMAGE" >> "$COMMIT_MSG_FILE" printf '\n%s %s\n' "$TRAILER_TOKEN_DIGEST" "$DIGEST_TO_TIMESTAMP" >> "$COMMIT_MSG_FILE" #request a timestamp token for each TSA defined TSA_IDX=-1 while : ; do ((TSA_IDX++)) TSA_URL=$(git config timestamping.tsa"$TSA_IDX".url) if [ -z "$TSA_URL" ]; then break fi echo_info "for TSA $TSA_URL" TOKEN_OPTIONAL=$(git config --type=bool timestamping.tsa"$TSA_IDX".optional) #retrieve token TOKEN_FILE="$TMP_DIR"/token.tst if ! request_token "$TSA_URL" "$DIGEST_TO_TIMESTAMP" false "$TOKEN_FILE"; then if [ ! "$TOKEN_OPTIONAL" ]; then echo_error "Error: Retrieving timestamp token for critical TSA$TSA_IDX failed." exit 1 else echo_warning "Warning: Retrieving timestamp token for optional TSA$TSA_IDX failed. Token won't be added." continue fi fi #validate token and download LTV data if ! verify_token_and_add_ltv_data "$TOKEN_FILE" "$DIGEST_TO_TIMESTAMP" "$TSA_URL"; then if [ ! "$TOKEN_OPTIONAL" ]; then echo_error "Error: Validating timestamp token for critical TSA$TSA_IDX failed." exit 1 else echo_warning "Warning: Validating timestamp token for optional TSA$TSA_IDX failed. Token won't be added." continue fi fi #add token to commit message openssl ts -reply -token_in -in "$TOKEN_FILE" -token_out -text -out "$TMP_DIR"/token.txt &> "$OUT_STREAM" #do not remove or change Info line (see license) INFO="Info: Timestamp generated with GitTrustedTimestamps by Mabulous GmbH" TOKENBASE64=$(openssl base64 -in "$TOKEN_FILE") TOKENTEXT=$(cat "$TMP_DIR"/token.txt) TRAILER_VALUE="$TSA_URL"$'\n'"$INFO"$'\n\n'"$TOKENTEXT"$'\n\n'"$TOKEN_HEADER"$'\n'"$TOKENBASE64"$'\n'"$TOKEN_FOOTER" #fold TRAILER_VALUE=$(printf "%s" "$TRAILER_VALUE" | sed -e 's/^/ /') printf '\n%s%s\n' "$TRAILER_TOKEN_TIMESTAMP" "$TRAILER_VALUE" >> "$COMMIT_MSG_FILE" done done #commit timestamps git commit --allow-empty --quiet -F "$COMMIT_MSG_FILE" echo_info "Timestamping complete" exit 0