Skip to content

Instantly share code, notes, and snippets.

@orangesurf
Created December 8, 2024 05:50
Show Gist options
  • Select an option

  • Save orangesurf/0d2bfa6336080d43189590bed2bf955b to your computer and use it in GitHub Desktop.

Select an option

Save orangesurf/0d2bfa6336080d43189590bed2bf955b to your computer and use it in GitHub Desktop.
directivo ots verifier

Document Verification Script

This script automates the verification of document timestamps using the OpenTimestamps (OTS) protocol and SimpleProof service. It is designed to process government documents from transparencia.gob.sv and verify their opentimestamps from simpleproof.

Running

  • install opentimestamps pip install opentimestamps-client
  • download directivo-ots-verifier.py & run python directivo-ots-verifier.py
  • (optional) update files/input.json with 2024 list
  • run python directivo-ots-verifier.py again
  • files/verified.json will be generated for all ots proofs that PASS the verifiaction

Core Functionality

  1. Reads document IDs from files/input.json
  2. For each document:
    • Downloads PDF from transparencia.gob.sv
    • Calculates SHA256 hash
    • Retrieves attestation from SimpleProof
    • Downloads corresponding OTS file stored by SimpleProof on AWS
    • Verifies timestamp against Bitcoin blockchain using mempool.space API
    • Saves verification results to files/verified.json

Key Features

  • Maintains document processing order from input.json
  • Skips previously verified documents
  • Creates organized directory structure (files/{document_id}/)
  • Handles pending attestations
  • Saves detailed verification results including block height, hash, and timestamp
  • Generates SimpleProof verification URLs for successfully verified documents

Error Handling

  • Graceful handling of network errors
  • Detailed error reporting for failed verifications
  • Preservation of existing verified results
  • Validation of API responses

Output Structure

Verification results in verified.json include:

  • File name
  • SHA256 hash
  • Verification status (PASS/FAIL/PENDING)
  • Bitcoin block details (height, hash, timestamp)
  • SimpleProof verification URL (for passed verifications)
import re
import requests
import hashlib
import json
import subprocess
from datetime import datetime, UTC
from urllib.parse import quote
from pathlib import Path
from collections import OrderedDict
def setup_directories(document_id):
"""Create directory structure for files"""
base_dir = Path("files")
doc_dir = base_dir / str(document_id)
base_dir.mkdir(exist_ok=True)
doc_dir.mkdir(exist_ok=True)
return base_dir, doc_dir
def initialize_workspace():
"""Create initial directory structure and sample input file"""
try:
# Create files directory
files_dir = Path("files")
files_dir.mkdir(exist_ok=True)
# Create sample input.json if it doesn't exist
input_path = files_dir / "input.json"
if not input_path.exists():
sample_input = [
"607369",
"607370"
]
with open(input_path, 'w') as f:
json.dump(sample_input, f, indent=2)
print("\nInitialized workspace:")
print("- Created 'files' directory")
print("- Created sample input.json")
print("\nPlease update files/input.json with your document IDs and run the script again")
return False
return True
except Exception as e:
print(f"Error initializing workspace: {str(e)}")
return False
def calculate_sha256(filename):
"""Calculate SHA256 hash of a file"""
sha256_hash = hashlib.sha256()
with open(filename, 'rb') as f:
for byte_block in iter(lambda: f.read(4096), b''):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def get_attestation(file_hash):
"""Get attestation from SimpleProof API"""
url = "https://app.simpleproof.com/api/proof/attestation"
payload = {
"category": "p-0097", # user ID
"hash": file_hash,
"num": 0
}
response = requests.post(url, json=payload)
return response.json()
def get_download_url(attestation):
"""Get download URL for OTS file"""
url = "https://app.simpleproof.com/api/proof/download-url"
payload = {
"space": attestation["space"],
"key": f"{attestation['prefix']}{attestation['otsName']}",
"size": attestation["otsSize"]
}
response = requests.post(url, json=payload)
return response.json()["url"]
def verify_timestamp_with_blockchain(pdf_file, ots_file, doc_dir):
"""Verify timestamp against blockchain using OTS CLI and mempool.space API"""
try:
# Run OTS verification and capture output
cmd = ['ots', '--no-bitcoin', 'verify', '-f', str(pdf_file), str(ots_file)]
result = subprocess.run(cmd, capture_output=True, text=True)
# Save the complete OTS output
ots_result_path = doc_dir / f"{pdf_file.stem}-ots-result.txt"
with open(ots_result_path, 'w') as f:
f.write(result.stderr)
merkle_roots = re.findall(r"merkleroot (\w{64})", result.stderr)
block_heights = re.findall(r"Bitcoin block (\d+) has", result.stderr)
if not merkle_roots or not block_heights:
return False, None, None, None
for height, merkleroot in zip(block_heights, merkle_roots):
try:
response = requests.get(f"https://mempool.space/api/block-height/{height}")
block_hash = response.text.strip()
block_data = requests.get(f"https://mempool.space/api/block/{block_hash}").json()
if merkleroot != block_data['merkle_root']:
return False, None, None, None
return True, int(height), block_hash, block_data['timestamp']
except Exception:
return False, None, None, None
except Exception:
return False, None, None, None
def process_document(document_id):
"""Process a document through the entire verification workflow"""
print(f"\nProcessing document {document_id}...")
try:
# Setup directories
base_dir, doc_dir = setup_directories(document_id)
# Download document
print("→ Downloading document...")
pdf_url = f"https://www.transparencia.gob.sv/institutions/capres/documents/{document_id}/download"
response = requests.get(pdf_url)
pdf_path = doc_dir / f"{document_id}.pdf"
with open(pdf_path, 'wb') as f:
f.write(response.content)
print("✓ Document downloaded")
# Calculate hash
print("→ Calculating SHA256...")
file_hash = calculate_sha256(pdf_path)
print(f"✓ SHA256: {file_hash}")
# Get attestation
print("→ Getting attestation...")
attestation = get_attestation(file_hash)
print("✓ Attestation received")
# Check attestation status
if attestation.get("status") == "document.status.pending":
print("\n! Document attestation is pending")
print("→ Document has not been stamped yet")
result_data = {
"sha256": file_hash,
"status": "PENDING",
"last_checked": datetime.now(UTC).isoformat()
}
return result_data
# Download OTS file
print("→ Downloading OTS file...")
try:
ots_url = get_download_url(attestation)
except KeyError as e:
print("\nError: Invalid attestation response")
print(f"Missing field: {str(e)}")
print("Attestation response content:")
print(json.dumps(attestation, indent=2))
raise Exception(f"Invalid attestation response - missing {str(e)} field") from e
except Exception as e:
print("\nError: Failed to get download URL")
print("Attestation response content:")
print(json.dumps(attestation, indent=2))
raise Exception("Failed to process attestation response") from e
ots_path = doc_dir / f"{document_id}.ots"
response = requests.get(ots_url)
with open(ots_path, 'wb') as f:
f.write(response.content)
print("✓ OTS file downloaded")
# Verify timestamp
print("→ Verifying blockchain timestamp...")
is_valid, block_height, block_hash, block_time = verify_timestamp_with_blockchain(pdf_path, ots_path, doc_dir)
verification_status = "PASS" if is_valid else "FAIL"
print(f"✓ Blockchain verification: {verification_status}")
# Create result data
result_data = {
"file_name": attestation["srcName"],
"sha256": file_hash,
"ots_verification": verification_status,
"block_height": block_height,
"block_hash": block_hash,
"block_time": block_time
}
# Add SimpleProof URL only if verification passed
if verification_status == "PASS":
simpleproof_url = f"https://verify.simpleproof.com/SP/p-0097/{file_hash}"
result_data["simpleproof-url"] = simpleproof_url
print("✓ Verification complete")
return result_data
except Exception as e:
if hasattr(e, '__cause__') and e.__cause__ is not None:
print(f"Error processing document {document_id}: {str(e)}")
else:
print(f"\nError processing document {document_id}:")
print(f"Error type: {type(e).__name__}")
print(f"Error message: {str(e)}")
if isinstance(e, requests.exceptions.RequestException):
print(f"URL: {e.request.url}")
print(f"Status code: {e.response.status_code if e.response else 'No response'}")
return None
def batch_process_documents():
"""Process all documents from input.json and update verified.json, maintaining input order"""
# Initialize workspace if needed
if not initialize_workspace():
return
try:
# Load input documents while preserving order
with open("files/input.json", 'r') as f:
document_ids = json.load(f)
# Initialize or load verified.json
verification_path = Path("files/verified.json")
# Create new ordered dict for results
verification_data = OrderedDict()
# If verification file exists, load existing PASS verifications
if verification_path.exists():
with open(verification_path, 'r') as f:
existing_verifications = json.load(f)
for doc_id, data in existing_verifications.items():
if data.get('ots_verification') == 'PASS':
verification_data[doc_id] = data
# Process each document in order
print(f"Found {len(document_ids)} documents to process")
documents_to_process = []
# First, check which documents need processing
for document_id in document_ids:
if (document_id in verification_data and
verification_data[document_id].get('ots_verification') == 'PASS'):
print(f"Skipping document {document_id} - already verified successfully")
else:
documents_to_process.append(document_id)
print(f"\nDocuments requiring verification: {len(documents_to_process)}")
# Process only the documents that need verification
for document_id in documents_to_process:
result = process_document(document_id)
if result and result.get('ots_verification') == 'PASS':
verification_data[document_id] = result
# Ensure order matches input.json after each verification
ordered_verification = OrderedDict()
for doc_id in document_ids:
if doc_id in verification_data:
ordered_verification[doc_id] = verification_data[doc_id]
# Save after each successful verification, with correct order
with open(verification_path, 'w') as f:
json.dump(ordered_verification, f, indent=2)
elif result:
# Document was processed but didn't pass verification
status = result.get('status', 'FAIL')
if status == 'PENDING':
print(f"Document {document_id} is pending stamping - not added to verified.json")
else:
print(f"Document {document_id} failed verification - not added to verified.json")
# Final order check and correction
final_verification = OrderedDict()
for doc_id in document_ids:
if doc_id in verification_data:
final_verification[doc_id] = verification_data[doc_id]
# Save final ordered version
with open(verification_path, 'w') as f:
json.dump(final_verification, f, indent=2)
print("\nBatch processing complete")
successfully_verified = len(final_verification)
print(f"Documents with successful verification: {successfully_verified}")
print(f"Documents pending or failed: {len(document_ids) - successfully_verified}")
print(f"Results saved to {verification_path} in original input order")
# Verify final order matches input.json
with open(verification_path, 'r') as f:
final_data = json.load(f)
final_order = list(final_data.keys())
input_order = [id for id in document_ids if id in final_data]
if final_order == input_order:
print("✓ Final verified.json order matches input.json")
else:
print("! Warning: Final order verification failed")
except Exception as e:
print(f"Error in batch processing: {str(e)}")
if __name__ == "__main__":
batch_process_documents()
[
"607370",
"607369",
"607368",
"607367",
"607366",
"607365",
"607364",
"607363",
"597319",
"607362",
"592974",
"607361",
"607360",
"592971",
"592967",
"597318",
"592962",
"597315",
"592955",
"592949",
"592937",
"592934"
]
{
"607370": {
"file_name": "source-Decreto_Ejecutivo_20_VP.pdf",
"sha256": "44a0bdb06ec94e19f81db8dccbe2ca18040a7f7807e14923803c174050e4273e",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/44a0bdb06ec94e19f81db8dccbe2ca18040a7f7807e14923803c174050e4273e"
},
"607369": {
"file_name": "source-Decreto_Ejecutivo_20_VP.pdf",
"sha256": "44a0bdb06ec94e19f81db8dccbe2ca18040a7f7807e14923803c174050e4273e",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/44a0bdb06ec94e19f81db8dccbe2ca18040a7f7807e14923803c174050e4273e"
},
"607368": {
"file_name": "source-Decreto_Ejecutivo_19_VP.pdf",
"sha256": "1eb87fb2d343b2253c1e8f833ae469026b0ffc3dfaac0fd7d6dcd40602ce4879",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/1eb87fb2d343b2253c1e8f833ae469026b0ffc3dfaac0fd7d6dcd40602ce4879"
},
"607367": {
"file_name": "source-Decreto_Ejecutivo_18_VP.pdf",
"sha256": "dd5e5e16caf53db51323ea7fdfa7a216b5c2907ee53a28629f089e82b00dfeea",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/dd5e5e16caf53db51323ea7fdfa7a216b5c2907ee53a28629f089e82b00dfeea"
},
"607366": {
"file_name": "source-Decreto_Ejecutivo_17_VP.pdf",
"sha256": "b44a4361dde1253176c3531bed762ab3074abf15f09bc7521171f045ad698799",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/b44a4361dde1253176c3531bed762ab3074abf15f09bc7521171f045ad698799"
},
"607365": {
"file_name": "source-Decreto_Ejecutivo_16.pdf",
"sha256": "119ed5eb4cb738a417becc18f11ddf98510faf7ebf83da2c0cc76dccdaedf9a9",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/119ed5eb4cb738a417becc18f11ddf98510faf7ebf83da2c0cc76dccdaedf9a9"
},
"607364": {
"file_name": "source-Decreto_Ejecutivo_15_VP.pdf",
"sha256": "1aa370004336e35010f9f53d6927c36d2a2ad230765e2524c33490e4a050b72d",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/1aa370004336e35010f9f53d6927c36d2a2ad230765e2524c33490e4a050b72d"
},
"607363": {
"file_name": "source-Decreto_Ejecutivo_12_VP.pdf",
"sha256": "7df06c003012e1697b084f55ccbb26b4de19d0a9fb74c2527a7a968c1423bec7",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/7df06c003012e1697b084f55ccbb26b4de19d0a9fb74c2527a7a968c1423bec7"
},
"597319": {
"file_name": "source-Decreto_Ejecutivo_No_11_VP.pdf",
"sha256": "e989dac57d65d29a14d9865f51c924d3c4cd14365f68364b8d8b3cde75030369",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/e989dac57d65d29a14d9865f51c924d3c4cd14365f68364b8d8b3cde75030369"
},
"607362": {
"file_name": "source-Decreto_Ejecutivo_10_VP.pdf",
"sha256": "006bc093de270370d198fb26fe0d0efe6f5e048a8f0d8575a080d6a62ca5e3cb",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/006bc093de270370d198fb26fe0d0efe6f5e048a8f0d8575a080d6a62ca5e3cb"
},
"592974": {
"file_name": "source-Decreto_Ejecutivo_10.pdf",
"sha256": "d6108e492647950c2c4fb18d701394a445214747ff1279de2770bac7100f24c9",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/d6108e492647950c2c4fb18d701394a445214747ff1279de2770bac7100f24c9"
},
"607361": {
"file_name": "source-Decreto_Ejecutivo_9_VP.pdf",
"sha256": "1e5e7caccfe3034ea4465b8c55d5c1d8bc6d5234840194371c07fde666bff297",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/1e5e7caccfe3034ea4465b8c55d5c1d8bc6d5234840194371c07fde666bff297"
},
"607360": {
"file_name": "source-Decreto_Ejecutivo_8_VP.pdf",
"sha256": "e1f6ffc47a47731edca291405b1c30c17a4be78e407180ae9c97c510816530d1",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/e1f6ffc47a47731edca291405b1c30c17a4be78e407180ae9c97c510816530d1"
},
"592971": {
"file_name": "source-Decreto_Ejecutivo_8.pdf",
"sha256": "2e7c7e9471b7380dd046945210dc5dd4a830d36eac1ae51bb78e06b56b01eed2",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/2e7c7e9471b7380dd046945210dc5dd4a830d36eac1ae51bb78e06b56b01eed2"
},
"592967": {
"file_name": "source-Decreto_Ejecutivo_7.pdf",
"sha256": "4eb0c49d3df9f544e0507b4b0b0eb0202b3433671c2c8cc34fff0f3867ba68c0",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/4eb0c49d3df9f544e0507b4b0b0eb0202b3433671c2c8cc34fff0f3867ba68c0"
},
"597318": {
"file_name": "source-Decreto_Ejecutivo_No_6_VP.pdf",
"sha256": "44e329245c72a74b654f6c9c0018a48a2624ccfd87e8ead1d151c8cffa6c98e4",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/44e329245c72a74b654f6c9c0018a48a2624ccfd87e8ead1d151c8cffa6c98e4"
},
"592962": {
"file_name": "source-Decreto_Ejecutivo_6.pdf",
"sha256": "e8c69f9acd9d7b726cb4f45387f985750fe5a9beac999628393894eabca7d4f7",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/e8c69f9acd9d7b726cb4f45387f985750fe5a9beac999628393894eabca7d4f7"
},
"597315": {
"file_name": "source-Decreto_Ejecutivo_No_5_VP.pdf",
"sha256": "a28dfc8b5488025dc57d0648def3f07ccbd34b3a80b0d79e7371855b26234169",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/a28dfc8b5488025dc57d0648def3f07ccbd34b3a80b0d79e7371855b26234169"
},
"592955": {
"file_name": "source-Decreto_Ejecutivo_4.pdf",
"sha256": "9e7fa234ceabf98d996ab4ae70a0eb92c25933734f377b5ee7566e83f043c9e0",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/9e7fa234ceabf98d996ab4ae70a0eb92c25933734f377b5ee7566e83f043c9e0"
},
"592949": {
"file_name": "source-Decreto_Ejecutivo_3.pdf",
"sha256": "5ebb3e3fa7864f9d7f9b25c46a5f43bd329cccf996fbb9ee7cbbc299645219e9",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/5ebb3e3fa7864f9d7f9b25c46a5f43bd329cccf996fbb9ee7cbbc299645219e9"
},
"592937": {
"file_name": "source-Decreto_Ejecutivo_2.pdf",
"sha256": "e281ad9c30a221a067ec834dcb3ef39ec5d6df18f3a33291513b39c0b82c5e3d",
"ots_verification": "PASS",
"block_height": 873440,
"block_hash": "0000000000000000000294dee95cc0beeceae6ad7068eafabea421c9c4091161",
"block_time": 1733446279,
"simpleproof-url": "https://verify.simpleproof.com/SP/p-0097/e281ad9c30a221a067ec834dcb3ef39ec5d6df18f3a33291513b39c0b82c5e3d"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment