Last active
June 19, 2026 07:52
-
-
Save NicolasCARPi/16869ab2e05e475d89d9e61fd8c4aab6 to your computer and use it in GitHub Desktop.
verify universign timestamp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Author: Nicolas CARPi / Deltablot | |
| # License: WTFPL | |
| set -euo pipefail | |
| # get selfsigned.pem file with: | |
| # curl -fsSLo universign-tsa-root-2019-selfsigned.pem https://www.universign.com/documents/universign-tsa-root-2019-selfsigned.pem | |
| usage() { | |
| cat >&2 <<'EOF' | |
| Usage: verify-universign-ts.sh <timestamped.json> <timestamp.asn1> | |
| Environment: | |
| TSA_ROOT=/path/to/universign-tsa-root-2019-selfsigned.pem | |
| Default: | |
| TSA_ROOT=./universign-tsa-root-2019-selfsigned.pem | |
| EOF | |
| } | |
| die() { | |
| echo "ERROR: $*" >&2 | |
| exit 1 | |
| } | |
| say() { | |
| printf '\n==> %s\n' "$*" | |
| } | |
| ok() { | |
| printf 'OK: %s\n' "$*" | |
| } | |
| need_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" | |
| } | |
| hex_digest() { | |
| local alg=$1 | |
| local file=$2 | |
| if command -v xxd >/dev/null 2>&1; then | |
| openssl dgst "-$alg" -binary "$file" | xxd -p -c 256 | |
| else | |
| openssl dgst "-$alg" -binary "$file" | od -An -tx1 -v | tr -d ' \n' | |
| printf '\n' | |
| fi | |
| } | |
| extract_imprint_hex() { | |
| local text_file=$1 | |
| awk ' | |
| BEGIN { in_msg = 0 } | |
| /^[[:space:]]*Message data:/ { | |
| in_msg = 1 | |
| next | |
| } | |
| in_msg == 1 && /^[[:space:]]*[0-9A-Fa-f]+ -/ { | |
| line = $0 | |
| sub(/^[[:space:]]*[0-9A-Fa-f]+ -[[:space:]]*/, "", line) | |
| sub(/[[:space:]]{3,}.*/, "", line) | |
| gsub(/[[:space:]-]/, "", line) | |
| printf "%s", tolower(line) | |
| next | |
| } | |
| in_msg == 1 && /^[[:space:]]*$/ { | |
| next | |
| } | |
| in_msg == 1 { | |
| exit | |
| } | |
| ' "$text_file" | |
| } | |
| parse_field() { | |
| local field=$1 | |
| local text_file=$2 | |
| awk -F':[[:space:]]*' -v field="$field" ' | |
| $1 ~ "^[[:space:]]*" field "$" { | |
| print $2 | |
| exit | |
| } | |
| ' "$text_file" | sed 's/[[:space:]]*$//' | |
| } | |
| to_epoch_utc() { | |
| local value=$1 | |
| if date -u -d "$value" '+%s' >/dev/null 2>&1; then | |
| date -u -d "$value" '+%s' | |
| return 0 | |
| fi | |
| if date -u -j -f '%b %e %H:%M:%S %Y %Z' "$value" '+%s' >/dev/null 2>&1; then | |
| date -u -j -f '%b %e %H:%M:%S %Y %Z' "$value" '+%s' | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| if [ "$#" -ne 2 ]; then | |
| usage | |
| exit 2 | |
| fi | |
| data_file=$1 | |
| asn1_file=$2 | |
| root_cert=${TSA_ROOT:-universign-tsa-root-2019-selfsigned.pem} | |
| [ -f "$data_file" ] || die "data file not found: $data_file" | |
| [ -f "$asn1_file" ] || die "ASN.1 timestamp file not found: $asn1_file" | |
| [ -f "$root_cert" ] || die "trusted TSA root not found: $root_cert" | |
| need_cmd openssl | |
| need_cmd awk | |
| need_cmd sed | |
| need_cmd grep | |
| need_cmd date | |
| workdir=$(mktemp -d) | |
| trap 'rm -rf "$workdir"' EXIT | |
| token_der="$workdir/timestamp-token.der" | |
| tst_text="$workdir/tstinfo.txt" | |
| tstinfo_der="$workdir/tstinfo.der" | |
| embedded_certs="$workdir/embedded-certs.pem" | |
| signer_pem="$workdir/signer.pem" | |
| intermediates_pem="$workdir/intermediates.pem" | |
| response_text="$workdir/response.txt" | |
| say 'checking the trusted TSA root certificate' | |
| openssl x509 -in "$root_cert" -noout -subject -issuer -serial -dates -fingerprint -sha256 | sed 's/^/ /' | |
| say 'extracting the timestamp token' | |
| if openssl ts -reply -in "$asn1_file" -token_out -out "$token_der" >/dev/null 2>"$workdir/extract-token.err"; then | |
| ok 'input is a full RFC 3161 TimeStampResp; extracted the embedded CMS token' | |
| if openssl ts -reply -in "$asn1_file" -text > "$response_text" 2>/dev/null; then | |
| echo 'Timestamp response status:' | |
| grep -E 'Status|Failure' "$response_text" | sed 's/^/ /' || true | |
| fi | |
| elif openssl cms -cmsout -inform DER -in "$asn1_file" -noout >/dev/null 2>"$workdir/probe-cms.err"; then | |
| cp "$asn1_file" "$token_der" | |
| ok 'input is already a CMS timestamp token' | |
| else | |
| cat "$workdir/extract-token.err" >&2 || true | |
| cat "$workdir/probe-cms.err" >&2 || true | |
| die 'input is neither a readable TimeStampResp nor a readable CMS timestamp token' | |
| fi | |
| say 'parsing timestamp information' | |
| openssl ts -reply -token_in -in "$token_der" -text > "$tst_text" | |
| policy_oid=$(parse_field 'Policy OID' "$tst_text") | |
| hash_alg=$(parse_field 'Hash Algorithm' "$tst_text" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') | |
| serial_number=$(parse_field 'Serial number' "$tst_text") | |
| timestamp_time=$(parse_field 'Time stamp' "$tst_text") | |
| token_imprint=$(extract_imprint_hex "$tst_text") | |
| [ -n "$hash_alg" ] || die 'could not read hash algorithm from timestamp token' | |
| [ -n "$token_imprint" ] || die 'could not read message imprint from timestamp token' | |
| echo " Policy OID: $policy_oid" | |
| echo " Hash algorithm: $hash_alg" | |
| echo " Serial number: $serial_number" | |
| echo " Time stamp: $timestamp_time" | |
| echo " Token message imprint: $token_imprint" | |
| say 'verifying the CMS signature using the RSA-PSS aware CMS verifier' | |
| if cms_output=$(openssl cms -verify -inform DER -in "$token_der" -noverify -out "$tstinfo_der" -signer "$signer_pem" 2>&1); then | |
| ok 'CMS signature is cryptographically valid' | |
| else | |
| printf '%s\n' "$cms_output" >&2 | |
| die 'CMS signature verification failed' | |
| fi | |
| if [ ! -s "$signer_pem" ]; then | |
| die 'OpenSSL verified the CMS signature but did not output the signer certificate' | |
| fi | |
| say 'extracting certificates embedded in the timestamp token' | |
| openssl pkcs7 -inform DER -in "$token_der" -print_certs -out "$embedded_certs" | |
| cert_count=$( | |
| awk -v dir="$workdir" ' | |
| /-----BEGIN CERTIFICATE-----/ { | |
| n++ | |
| file = sprintf("%s/embedded-cert-%02d.pem", dir, n) | |
| } | |
| n > 0 { | |
| print > file | |
| } | |
| /-----END CERTIFICATE-----/ { | |
| close(file) | |
| } | |
| END { | |
| print n + 0 | |
| } | |
| ' "$embedded_certs" | |
| ) | |
| [ "$cert_count" -gt 0 ] || die 'no embedded certificates found in timestamp token' | |
| ok "found $cert_count embedded certificate(s)" | |
| : > "$intermediates_pem" | |
| for cert in "$workdir"/embedded-cert-*.pem; do | |
| if openssl x509 -in "$cert" -noout -ext basicConstraints 2>/dev/null | grep -q 'CA:TRUE'; then | |
| cat "$cert" >> "$intermediates_pem" | |
| fi | |
| done | |
| say 'checking the TSA signer certificate' | |
| openssl x509 -in "$signer_pem" -noout -subject -issuer -serial -dates -fingerprint -sha256 -ext extendedKeyUsage -ext basicConstraints | sed 's/^/ /' | |
| eku_text=$(openssl x509 -in "$signer_pem" -noout -ext extendedKeyUsage 2>/dev/null || true) | |
| printf '%s\n' "$eku_text" | grep -q 'Time Stamping' || die 'signer certificate does not contain the Time Stamping extended key usage' | |
| printf '%s\n' "$eku_text" | grep -qi 'critical' || die 'signer certificate Time Stamping EKU is not marked critical' | |
| ok 'signer certificate has critical Time Stamping EKU' | |
| say 'verifying the TSA signer certificate chain and timestamping purpose' | |
| verify_cmd=(openssl verify -purpose timestampsign -CAfile "$root_cert") | |
| if [ -n "$timestamp_time" ] && attime=$(to_epoch_utc "$timestamp_time"); then | |
| verify_cmd+=(-attime "$attime") | |
| echo " Verifying certificate validity at timestamp time: $timestamp_time" | |
| else | |
| echo ' WARNING: could not parse timestamp time; verifying certificate validity at current time' | |
| fi | |
| if [ -s "$intermediates_pem" ]; then | |
| verify_cmd+=(-untrusted "$intermediates_pem") | |
| fi | |
| verify_cmd+=("$signer_pem") | |
| if chain_output=$("${verify_cmd[@]}" 2>&1); then | |
| printf ' %s\n' "$chain_output" | |
| ok 'TSA signer certificate chains to the configured trusted root and is valid for timestamp signing' | |
| else | |
| printf '%s\n' "$chain_output" >&2 | |
| die 'TSA signer certificate chain or timestamping purpose verification failed' | |
| fi | |
| say 'computing the local digest of the JSON file' | |
| case "$hash_alg" in | |
| sha1|sha224|sha256|sha384|sha512|sha3-224|sha3-256|sha3-384|sha3-512) | |
| ;; | |
| *) | |
| die "unsupported or unexpected hash algorithm: $hash_alg" | |
| ;; | |
| esac | |
| local_digest=$(hex_digest "$hash_alg" "$data_file" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') | |
| echo " Local $hash_alg digest: $local_digest" | |
| echo " Token message imprint: $token_imprint" | |
| if [ "$local_digest" = "$token_imprint" ]; then | |
| ok 'JSON digest matches the timestamp token message imprint' | |
| else | |
| die 'JSON digest does not match the timestamp token message imprint' | |
| fi | |
| say 'final result' | |
| echo 'FINAL RESULT: OK' | |
| echo 'The timestamp token signature is valid, the TSA signer chains to the configured trusted root,' | |
| echo 'the signer certificate is valid for timestamp signing, and the JSON file matches the timestamp imprint.' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment