Verify a Signature Without Mark
Step-by-step instructions for independently verifying a Mark signature using only public tools and blockchains. No Mark account, server, or app required.
What you need
A Mark signature is a self-contained JSON file called a SignedBundle. Everything required to verify it is either inside the file or on the public Polygon blockchain. This guide walks through the full verification using standard command-line tools.
Tools used: openssl (or any Ed25519 library), jq, sha256sum, and cast (from Foundry) for Polygon queries. Python or Node.js work too.
Step 1: Inspect the signed bundle
Open the SignedBundle JSON file. The key fields are:
{
"type": "SignedBundle",
"version": 2,
"document_hash": "a1b2c3...64-char hex...",
"signature": "base64-encoded Ed25519 signature",
"signed_input": {
"document_hash": "a1b2c3...",
"timestamp": "2026-06-08T12:00:00Z",
"signer_fingerprint": "d4e5f6...",
"service_record_hash": "...",
"nonce": "base64..."
},
"subkey_certificate": {
"fingerprint": "d4e5f6...",
"public_key": "base64-encoded Ed25519 public key",
"signed_by_root_fingerprint": "9e2b...",
"root_signature": "base64...",
"valid_from": "2026-01-01T00:00:00Z",
"valid_until": "2027-01-01T00:00:00Z"
},
"identity_certificate_pointer": "polygon:9e2b..."
}Step 2: Verify the document hash
If you have the original document, compute its SHA-256 hash and compare:
sha256sum original-document.pdf
# Compare the output to bundle.document_hashIf the hashes match, the document is the exact file that was signed. If you don't have the original document, you can still verify everything else — you just can't confirm which document was signed.
Step 3: Verify the signature
The signature is an Ed25519 signature over the canonicalized signed_input JSON.
Canonicalization (MCJ): Sort all keys lexicographically, no whitespace, nulls explicitly present. For example:
# Extract signed_input, canonicalize, and verify
# Using Python:
import json, base64, hashlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
bundle = json.load(open("bundle.json"))
signed_input = bundle["signed_input"]
# Canonicalize: sort keys, compact separators
canonical = json.dumps(signed_input, sort_keys=True, separators=(",", ":"))
# Get the signing subkey's public key
pubkey_b64 = bundle["subkey_certificate"]["public_key"]
pubkey_bytes = base64.b64decode(pubkey_b64)
# Decode the signature
sig_b64 = bundle["signature"]
sig_bytes = base64.b64decode(sig_b64)
# Verify
key = Ed25519PublicKey.from_public_key_bytes(pubkey_bytes)
key.verify(sig_bytes, canonical.encode("utf-8"))
print("Signature VALID")If key.verify() does not raise an exception, the signature is mathematically valid — the person who controls this subkey signed this exact data.
Step 4: Verify the subkey certificate
The subkey is authorized by a root key. The subkey_certificate contains a root_signature proving this. You need the root public key — which is on the Polygon blockchain.
# The subkey certificate's signed message is the certificate itself
# (minus the root_signature field), canonicalized.
cert = bundle["subkey_certificate"].copy()
root_sig = base64.b64decode(cert.pop("root_signature"))
canonical_cert = json.dumps(cert, sort_keys=True, separators=(",", ":"))
# You'll get the root public key in Step 5. For now, save this.Step 5: Fetch the service record from Polygon
The signer's identity (service record) is published on the Polygon blockchain as an event log. The identity_certificate_pointer field tells you the root fingerprint.
# ServiceRecordRegistry V3 contract on Polygon
REGISTRY="0x1e0524143B951c135e17F7D30fD5f6dc70ad4B80"
# The event signature for ServiceRecordPublished
# keccak256("ServiceRecordPublished(bytes32,string)")
TOPIC="0x$(cast keccak "ServiceRecordPublished(bytes32,string)")"
# The root fingerprint (from identity_certificate_pointer, e.g. "polygon:9e2b...")
ROOT_FP="0x9e2b...the full 64-char fingerprint..."
# Query Polygon for the latest event
cast logs \
--from-block 50000000 \
--rpc-url https://polygon-rpc.com \
$REGISTRY \
$TOPIC \
$ROOT_FPThe event data contains the full service record JSON. Decode it to get:
- root_public_key — the Ed25519 public key for the root identity
- active_subkeys — list of currently valid signing subkeys
- revoked_subkeys — list of revoked subkeys
Step 6: Verify root authorized the subkey
Now that you have the root public key from the chain, verify the subkey certificate:
# Using the root public key from the service record
root_pubkey_bytes = base64.b64decode(service_record["root_public_key"])
root_key = Ed25519PublicKey.from_public_key_bytes(root_pubkey_bytes)
# Verify the root signed the subkey certificate
root_key.verify(root_sig, canonical_cert.encode("utf-8"))
print("Subkey certificate VALID — root authorized this subkey")Step 7: Check subkey validity and revocation
Confirm the subkey was valid at signing time and has not been revoked:
signing_time = bundle["signed_input"]["timestamp"] # ISO 8601
valid_from = bundle["subkey_certificate"]["valid_from"]
valid_until = bundle["subkey_certificate"]["valid_until"]
# 1. Check validity window
assert valid_from <= signing_time <= valid_until, "Subkey not valid at signing time"
# 2. Check revocation list from service record
subkey_fp = bundle["subkey_certificate"]["fingerprint"]
revoked = service_record.get("revoked_subkeys", [])
for r in revoked:
if r["fingerprint"] == subkey_fp:
if r.get("reason") == "compromised":
print("WARNING: Subkey was revoked for COMPROMISE")
else:
print("Note: Subkey revoked after signing (non-compromise) — signature still valid")
break
else:
print("Subkey is active and not revoked")Step 8: Check on-chain bundle (optional)
If the signature was published on-chain (most are by default), you can confirm it exists in the MarkSignatureBundleRegistry:
# BundleRegistry contract on Polygon
BUNDLE_REG="0x228edB94dE4fcF55F4fE1584A6a8b2d201a0276F"
# Event: SignatureBundlePublished(bytes32 documentHash, bytes32 signerFingerprint, string bundleJson)
BUNDLE_TOPIC="0x$(cast keccak "SignatureBundlePublished(bytes32,bytes32,string)")"
# Query by document hash
DOC_HASH="0x$(jq -r '.document_hash' bundle.json)"
cast logs \
--from-block 50000000 \
--rpc-url https://polygon-rpc.com \
$BUNDLE_REG \
$BUNDLE_TOPIC \
$DOC_HASHIf an event exists, the Polygon block timestamp provides independent temporal proof of when the signature was published. This cannot be backdated.
Verification summary
If all checks pass, you have proven:
- Integrity — the document hash matches (the document was not tampered with)
- Authenticity — the Ed25519 signature is valid (someone with this key signed it)
- Authorization — the root key authorized the signing subkey (the subkey certificate is valid)
- Identity persistence — the root key's service record is on the Polygon blockchain (the identity is discoverable and immutable)
- Non-revocation — the signing subkey has not been revoked for compromise
- Temporal proof — the Polygon block timestamp anchors when the proof was published
None of this depends on Mark's servers, database, or continued existence. The math, the blockchain, and the open protocol are all you need.
Contract addresses
Both contracts are on Polygon mainnet (chain ID 137):
- ServiceRecordRegistry V3:
0x1e0524143B951c135e17F7D30fD5f6dc70ad4B80 - MarkSignatureBundleRegistry:
0x228edB94dE4fcF55F4fE1584A6a8b2d201a0276F
You can query these with any Ethereum-compatible tool: Polygonscan, cast (Foundry), ethers.js, web3.py, or a direct JSON-RPC call to any Polygon node.