Sealing Contracts
The problem
You need to prove a document hasn't been tampered with since it was signed. If someone changes a word, you need to know.
The solution
dotit seal computes a SHA-256 hash of the document content, adds a sign: block and a freeze: block. dotit verify checks the hash against the current content.
Seal
dotit seal contract.it --signer "Ahmed Al-Rashid" --role "CEO"
This adds to the document:
sign: Ahmed Al-Rashid | role: CEO | at: 2026-03-22T15:00:00Z | hash: sha256:a1b2c3d4e5f6a7b8 | spec: 4
freeze: | at: 2026-03-22T15:00:00Z | hash: sha256:f9a0b1c2d3e4f5a6 | spec: 4 | appearance: sha256:236e82f454aa492d | status: locked
The freeze: line also carries an appearance: hash in spec: 4. The seal hash:
excludes styling (so restyling never breaks it); the appearance: hash covers the document
with styling, so a hidden-content restyle is flagged (appearanceChanged) without
invalidating the seal. See What the hash covers.
Verify
dotit verify contract.it
Output when valid:
✓ Document sealed at 2026-03-22T15:00:00Z
✓ Hash valid: sha256:a1b2c3d4e5f6a7b8
✓ 1 signature: Ahmed Al-Rashid (CEO)
✓ No amendments
Output when tampered:
✗ SEAL BROKEN
Expected: sha256:a1b2c3d4e5f6a7b8
Actual: sha256:9c8d7e6f5a4b3c2d
The document has been modified since sealing.
What the hash covers
The hash is computed from document content above the history boundary, excluding trust
metadata, comments, and presentation. The current ruleset is spec: 4. It covers:
title:,summary:,meta:blocks- All section content
- All block content and content properties
approve:blocks
The hash does not cover:
sign:/freeze:/certify:/amendment:lines (the seal/signature scope keeps thefreeze:line with its ownhash:blanked; everything else is stripped)- Styling — presentation lines (
page:,font:,style:) and presentation properties (color,size,align,margin,leading, …). Restyling never breaks a seal — "sign content, not presentation." - Comments (
//lines) — including the optional// it-format: 1.0version stamp, a leading-header comment that self-describes the grammar version for long-term archives. Because it's a comment, adding or changing it never breaks a seal (the parser exposes it asdocument.version). - The
history:boundary and revisions below it
This is what makes amendments possible: amendment: lines are excluded from the content
(like sign: and freeze:), so adding one never breaks the original seal. Likewise,
re-theming or reformatting a sealed contract leaves its seal intact.
Two scopes share this algorithm: each sign: line hashes the content (and binds the
signer's name | role | at), while the freeze: line hashes the seal scope — the
content plus the signatures plus the seal's own metadata.
A seal is therefore a SHA-256 hash over the document's content under spec: 4. v4
excludes styling and comments and normalizes line endings (CRLF/CR → LF) and trailing
whitespace — so restyling, reformatting, a CRLF round-trip, or a trailing-space re-save
never break a seal; only a content change does. The separate appearance: hash (the
full-fidelity hash, styling included) is what flags a hidden-content restyle: if it changes
but the content hash still matches, verifyDocument keeps intact: true and sets
appearanceChanged.
Multiple signatures
A document can have multiple signers:
# First signer
dotit seal contract.it --signer "Ahmed Al-Rashid" --role "CEO, Acme Corp"
# Second signer (adds another sign: block, re-computes freeze:)
dotit seal contract.it --signer "Maria Santos" --role "COO, GlobalTech"
After both:
sign: Ahmed Al-Rashid | role: CEO, Acme Corp | at: 2026-03-22T10:00:00Z | hash: sha256:a1b2c3d4 | spec: 4
sign: Maria Santos | role: COO, GlobalTech | at: 2026-03-22T14:30:00Z | hash: sha256:e5f6a7b8 | spec: 4
freeze: | at: 2026-03-22T14:30:00Z | hash: sha256:c3d4e5f6 | spec: 4 | appearance: sha256:236e82f4 | status: locked
Verification in code
import { verifyDocument } from "@dotit/core";
// verifyDocument takes the raw .it source string and re-derives the content hash
const result = verifyDocument(source);
if (result.intact) {
console.log("Seal intact:", result.hash);
for (const s of result.signers ?? []) {
console.log(`${s.signer} (${s.role}) — valid: ${s.valid}`);
}
} else {
console.log("SEAL BROKEN");
console.log("Expected:", result.expectedHash);
console.log("Actual: ", result.hash);
}
To seal in code, use sealDocument(source, { signer, role }) — it returns { success, hash, source, at }. Store the returned source and you're done. Because spec: 4 normalizes line endings (CRLF/CR → LF) and trailing whitespace before hashing, a later LF↔CRLF round-trip (Windows git autocrlf, an email gateway) or a trailing-space re-save will not break the seal — only a content change will.
Complete workflow
# 1. Write the contract
# 2. Review and add approvals (manually or via editor)
# 3. Seal
dotit seal contract.it --signer "Ahmed Al-Rashid" --role "CEO"
# 4. Send to counterparty, they seal too
dotit seal contract.it --signer "Maria Santos" --role "COO"
# 5. Verify at any time
dotit verify contract.it
# 6. View full history
dotit history contract.it
Next steps
- Amending Frozen Documents — when a sealed contract needs changes
- Approval Workflow — the full approve → sign → freeze flow
- Attachments — embed files so the seal covers them too
- Conformance — pass the strict gate before sealing
- Redline & Compare — review changes before re-sealing
- Audit Trail — revision tracking