Manual verification
This page describes the offline verification algorithm in full. It is the authoritative reference for anyone who wants to verify a Doc E Sign document without using the online verification page.
The algorithm requires only the signed PDF. No Doc E Sign account, no internet connection, no external tools beyond a SHA-256 implementation.
The Signing chain fingerprint printed on the audit page is not a direct SHA-256 hash of the signed PDF. It is the final value in a hash chain computed across the complete signing process. Step 5 of the algorithm below shows exactly how it is derived.
What you need
- The signed PDF
- A SHA-256 implementation (available in every major programming language and as a command-line tool on all operating systems)
The algorithm
1. From the audit page, read pre_audit_byte_length (integer).
2. Extract bytes [0 .. pre_audit_byte_length − 1] from the PDF → signed_pdf_bytes.
3. Compute: sealed_pdf_hash = lowercase_hex(SHA-256(signed_pdf_bytes))
4. Set H = Original document fingerprint (from the audit page).
5. For each event in the event log (in the order listed on the audit page):
If event_type is "completed":
payload = sealed_pdf_hash (computed in step 3 — do not use the value from the audit page)
Otherwise:
payload = the payload value as printed in the audit page event log
H = lowercase_hex(SHA-256(H + "|" + event_type + "|" + timestamp + "|" + payload))
6. Assert H equals the Signing chain fingerprint printed on the audit page.
If the assertion in step 6 passes, the document is unaltered and the event log is complete and correct.
If it fails, either the PDF or the audit page has been modified since signing completed.
Field formats
| Field | Format |
|---|---|
pre_audit_byte_length | Integer; printed on the audit page |
Event timestamp | ISO 8601 UTC, no fractional seconds — e.g. 2026-05-30T14:22:05Z |
Event payload | Either a lowercase hex string, or the literal value none |
| Hash values | Lowercase hex SHA-256 — 64 characters |
The | separator in step 5 is a literal ASCII pipe character (byte 0x7C). Concatenation uses no encoding — raw string concatenation.
What verification proves
- Step 3 independently verifies that the pre-audit-page PDF bytes match the sealed document. If the signed PDF has been altered since signing,
sealed_pdf_hashwill differ from what the chain expects, and the final assertion will fail. - Step 5 verifies that the event log is complete, in order, and unaltered. Changing any event's type, timestamp, or payload — or inserting or removing an event — produces a different Signing chain fingerprint.
What it does not prove
Identity HMAC payloads (for link_clicked and signed events) are pre-computed values stored on the audit page. The chain verifies their inclusion and order but does not reveal the underlying data (IP address, user agent). Independently verifying that a specific HMAC corresponds to specific data requires Doc E Sign's HMAC secret or the online verification endpoint.
Reference implementations
JavaScript (Node.js built-in crypto)
const { createHash } = require('crypto');
const { readFileSync } = require('fs');
function verify(pdfPath, auditPage) {
const pdfBytes = readFileSync(pdfPath);
// Step 2–3: extract pre-audit bytes and hash them
const preAuditBytes = pdfBytes.slice(0, auditPage.preAuditByteLength);
const sealedPdfHash = createHash('sha256').update(preAuditBytes).digest('hex');
// Step 4: start with Hash 1
let h = auditPage.originalDocumentFingerprint;
// Step 5: replay the chain
for (const event of auditPage.events) {
const payload = event.type === 'completed' ? sealedPdfHash : event.payload;
const input = `${h}|${event.type}|${event.timestamp}|${payload}`;
h = createHash('sha256').update(input).digest('hex');
}
// Step 6: assert
const valid = h === auditPage.signingChainFingerprint;
console.log(valid ? 'VALID' : 'INVALID');
return valid;
}
Python (hashlib)
import hashlib
def verify(pdf_path: str, audit_page: dict) -> bool:
with open(pdf_path, 'rb') as f:
pdf_bytes = f.read()
# Steps 2–3: extract pre-audit bytes and hash them
pre_audit_bytes = pdf_bytes[:audit_page['pre_audit_byte_length']]
sealed_pdf_hash = hashlib.sha256(pre_audit_bytes).hexdigest()
# Step 4: start with Hash 1
h = audit_page['original_document_fingerprint']
# Step 5: replay the chain
for event in audit_page['events']:
payload = sealed_pdf_hash if event['type'] == 'completed' else event['payload']
input_str = f"{h}|{event['type']}|{event['timestamp']}|{payload}"
h = hashlib.sha256(input_str.encode()).hexdigest()
# Step 6: assert
valid = h == audit_page['signing_chain_fingerprint']
print('VALID' if valid else 'INVALID')
return valid
Shell (sha256sum / shasum)
This shell script demonstrates the algorithm for a single-event chain. For a real document with multiple events, loop over the event log entries from the audit page.
#!/bin/bash
set -euo pipefail
PDF="$1"
PRE_AUDIT_LENGTH="$2" # pre_audit_byte_length from the audit page
HASH1="$3" # Original document fingerprint
EVENT_TYPE="$4" # e.g. "completed"
EVENT_TIMESTAMP="$5" # e.g. "2026-05-30T14:22:05Z"
EVENT_PAYLOAD="$6" # payload from audit page (or "none")
EXPECTED_HASH2="$7" # Signing chain fingerprint
# Steps 2–3: hash the pre-audit bytes
SEALED_PDF_HASH=$(dd if="$PDF" bs=1 count="$PRE_AUDIT_LENGTH" 2>/dev/null | sha256sum | cut -d' ' -f1)
# For "completed" events, use the sealed PDF hash as payload
if [ "$EVENT_TYPE" = "completed" ]; then
PAYLOAD="$SEALED_PDF_HASH"
else
PAYLOAD="$EVENT_PAYLOAD"
fi
# Step 5: compute H[i]
CHAIN_INPUT="${HASH1}|${EVENT_TYPE}|${EVENT_TIMESTAMP}|${PAYLOAD}"
COMPUTED=$(printf '%s' "$CHAIN_INPUT" | sha256sum | cut -d' ' -f1)
# Step 6: compare
if [ "$COMPUTED" = "$EXPECTED_HASH2" ]; then
echo "VALID"
else
echo "INVALID"
echo "Expected: $EXPECTED_HASH2"
echo "Computed: $COMPUTED"
exit 1
fi
Online verification
As an alternative to running the algorithm yourself, paste the Signing chain fingerprint into doc-e-sign.com/verify. The online endpoint performs the same chain replay using Doc E Sign's database and returns the signing context (date, document title, signer email domain).
Online verification produces the same integrity conclusion as offline verification. The offline algorithm does not depend on the online endpoint — it is provided as a convenience.