Tamper-evident invoice hash chains for Spain's VeriFactu (RD 1007/2023). Zero runtime dependencies · fully typed · 98% test coverage · fails closed.
Spain's anti-fraud law RD 1007/2023 (the VeriFactu regime) requires billing software to make issued invoices inalterable and traceable: each record must be cryptographically chained to the previous one, so any later deletion or edit is detectable.
Most teams reinvent this — usually with a subtly non-deterministic hash that breaks the moment a timezone or a decimal format changes. verifactu-core is the small, correct, dependency-free primitive that does only this, and does it right.
Scope: the integrity core (canonicalization + hash chain + verification). XAdES signing and AEAT transport are deliberately out of scope — they are replaceable adapters; the chain is forever.
pip install verifactu-core # once published to PyPI
# or, from source:
pip install -e ".[dev]"No third-party runtime dependencies. Python 3.11+.
from datetime import datetime, timezone
from verifactu_core import InvoiceChain, InvoiceRecord
chain = InvoiceChain()
chain.append(InvoiceRecord(
issuer_nif="B12345678",
series="A",
number="0001",
issue_datetime=datetime(2026, 1, 15, 10, 30, tzinfo=timezone.utc),
recipient_nif="X1234567L",
total="121.00",
tax="21.00",
description="Consulting services",
))
chain.verify() # raises ChainBrokenError if tampered
print(chain.head_hash) # 64-char sha256 hexSee examples/quickstart.py for a full build → verify → tamper-detection demo.
flowchart LR
R1["record #0<br/>prev = GENESIS"] --> R2["record #1<br/>prev = hash(#0)"]
R2 --> R3["record #2<br/>prev = hash(#1)"]
R3 --> R4["..."]
style R1 fill:#2a9d8f,color:#fff
style R2 fill:#2a9d8f,color:#fff
style R3 fill:#2a9d8f,color:#fff
record_hash = sha256( previous_hash ‖ 0x1E ‖ canonical(record) )
genesis previous_hash = "0" * 64
Three guarantees, by construction:
| Guarantee | Mechanism |
|---|---|
| Determinism | Frozen, versioned canonical format (fixed field order, UTC ISO-8601, 2-dp decimals, NFC text, 0x1F separator that is illegal in every field) |
| Tamper-evidence | Previous hash bound into the digest — altering record N invalidates N..end and locate_tamper() returns N |
| Fail-closed | Every malformed input or broken link raises; nothing is ever silently accepted |
| Symbol | Purpose |
|---|---|
InvoiceRecord |
Immutable, validated invoice (NIF shape, tz-aware datetime, 2-dp money, tax <= total) |
InvoiceChain |
Append-only chain: .append(), .verify(), .locate_tamper(), .head_hash |
compute_hash(record, prev) |
Pure chain-link function |
canonicalize(record) |
The deterministic canonical string (v1, frozen) |
verify_chain(records) |
Stateless verification of an externally stored chain |
ChainBrokenError.index |
Zero-based index of the first broken record |
pytest # 31 tests
pytest --cov=verifactu_core # 98% coverageThe suite asserts the security property the law cares about, not just "the code runs":
test_tamper.py— altering, deleting or reordering any record is detected and localizedtest_canonical.py— timezone, decimal precision, Unicode NFC and control-char injectiontest_models.py— every validation invariant fails closed
-
to_aeic_dict()AEAT record mapper (optional extra) - XAdES signing adapter (separate package, keeps core dep-free)
- QR payload helper per the VeriFactu spec
- Property-based tests (Hypothesis) for canonicalization
- PyPI release
0.1.0
MIT © Francisco Amaro Prieto
Companion work: VeriFactu-Integrity-Lab (write-up) · VeriStack · Barista case study
Built by Francisco Amaro — Backend Engineer & SOC L1 Analyst LinkedIn · Email