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 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​