Skip to main content

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 the freeze: line with its own hash: 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.0 version 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 as document.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 LFCRLF 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