Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for hash chaining to detect modifications in postings #2300

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f4c56d1
Add support for hash chaining to detect modifications in postings
jwiegley Nov 23, 2023
0ddea87
--hashes option requires an argument to specify the algorithm
jwiegley Nov 23, 2023
e2e4716
Add positive and negative tests for the --hashes option
jwiegley Nov 23, 2023
853374b
Add documentation for the --hashes option
jwiegley Nov 23, 2023
55287a0
Make xact hashes independent of posting order
jwiegley Nov 25, 2023
8f2d712
Improvements to the hashing tests
jwiegley Nov 27, 2023
0c808e0
Add further documentation on the --hashes option
jwiegley Nov 28, 2023
28c10eb
Add support for --hashes=sha512_256 as another algorithm
jwiegley Nov 30, 2023
d8c341d
Update doc/ledger.1
jwiegley Dec 6, 2023
d4eff3d
Update doc/ledger.1
jwiegley Dec 6, 2023
845ccb5
Update src/sha512.cc
jwiegley Dec 6, 2023
ce5664e
Update src/sha512.cc
jwiegley Dec 11, 2023
b5161c9
Minor doc update
jwiegley Dec 11, 2023
c729035
Type signature fix
jwiegley Dec 12, 2023
0f723d2
Revert a type change
jwiegley Dec 12, 2023
43a173d
Try something else
jwiegley Dec 12, 2023
8b346cf
Fix return type of SHA512
jwiegley Dec 12, 2023
82f98d7
Include stdint.h in sha512.cc
jwiegley Dec 12, 2023
c5fa5fa
Include sys/types.h
jwiegley Dec 12, 2023
2499963
Remove most changes to sha512.cc
jwiegley Dec 12, 2023
bea3498
Change one prototype
jwiegley Dec 12, 2023
8d43d4c
Revert all changes to sha512.c
jwiegley Dec 12, 2023
4514248
Add two missing system headers to sha512.cc
jwiegley Dec 12, 2023
f24640b
Rename SHA-512/256 to the more appropriate SHA-512Half
jwiegley Dec 13, 2023
cf0fadf
Add whitespace to xact_t::hash
jwiegley Dec 13, 2023
60010af
Another whitespace change
jwiegley Dec 13, 2023
2f732b9
Merge remote-tracking branch 'origin/master' into johnw/hashes
jwiegley Jan 4, 2024
a098d7f
Expand the size of an arbitrary safety limit
jwiegley Jan 18, 2024
baddd0e
Change an assertion into an if test
jwiegley Jan 18, 2024
33a70ca
Merge remote-tracking branch 'origin/master' into johnw/hashes
jwiegley Jan 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion doc/ledger.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.Dd March 15, 2019
.Dd November 30, 2023
.Dt LEDGER 1
.Os
.Sh NAME
Expand Down Expand Up @@ -634,6 +634,24 @@ Set the format for the headers that separate reports section of
a grouped report. Only has effect with a
.Fl \-group-by Ar EXPR
register report.
.It Fl \-hashes Ar ALGO
Record the hash of each transaction in a
.Ar Hash
metadata value, according to the hashing algorithm given by the
.Ar ALGO
argument. Support algorithms are
.Ar sha512
jwiegley marked this conversation as resolved.
Show resolved Hide resolved
or
.Ar sh512_256
where both use SHA512, but the latter only stores the first half of
jwiegley marked this conversation as resolved.
Show resolved Hide resolved
the hash value.
.Pp
If a
.Ar Hash
metadata value is explicitly provided and does not match what would
have been generated, an error is reported. Hashes depend on previous
entries, such that setting a single hash value is sufficient to
guarantee the shape of the entire history leading up to that entry.
.It Fl \-head Ar INT
jwiegley marked this conversation as resolved.
Show resolved Hide resolved
Print the first
.Ar INT
Expand Down
51 changes: 51 additions & 0 deletions doc/ledger3.texi
Original file line number Diff line number Diff line change
Expand Up @@ -6697,6 +6697,57 @@ $ ledger reg Expenses --group-by "payee" --group-title-format "-----------------
...
@end smallexample

@item --hashes @var{ALGO}
Records the chained hash of each transaction in a @var{Hash} metadata
value, according to the hashing algorithm given by the @var{ALGO}
argument (at the moment, only @code{sha512} is supported). To use this,
record the @var{Hash} metadata explicitly in some of your transactions;
these will be checked against the hashes calculated internally, and if
they do not match, an error is reported. You may also write just a
prefix of the @var{Hash}, which is less verbose but still gives quite
good assurance.

The support algorithms are:

@table @code
@item sha512
Use the SHA512 hashing algorithm.
@item sha512_256
jwiegley marked this conversation as resolved.
Show resolved Hide resolved
Same as SHA512, but record only the first 256 bits.
@end table

Somewhat like balance assertions, which give assurance that previous
posting amounts are correct, these @var{Hash} tags give assurance that
all previous journal entries (in parse order) are unchanged (or at
least, their combined hash matches the Hash tag currently appearing in
the journal).

These hashes depend on the hashes of previous transactions, such that
the single hash value of the final transaction is sufficient to
guarantee the shape of the entire history leading up to it.

The other details that the hash depends on are the following details
from each posting in the transaction:

@itemize
@item fullname of the account
@item amount value
@end itemize

In addition, these details are hashed from the transaction itself:

@itemize
@item actual date
@item auxiliary date (if provided)
jwiegley marked this conversation as resolved.
Show resolved Hide resolved
@item code (if provided)
@item payee
@end itemize

This list also means that changes in the comments of postings or
transactions, or in the ordering of the postings within a transaction,
will not affect the hash. The ordering of the transactions does matter,
however, the same way as it does for balance assertions.

jwiegley marked this conversation as resolved.
Show resolved Hide resolved
@item --head @var{INT}
@itemx --first @var{INT}
Print the first @var{INT} entries. Opposite of @option{--tail
Expand Down
3 changes: 2 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ set(LEDGER_SOURCES
times.cc
error.cc
utils.cc
wcwidth.cc)
wcwidth.cc
sha512.cc)

if (HAVE_GPGME)
list(APPEND LEDGER_SOURCES
Expand Down
2 changes: 1 addition & 1 deletion src/generate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ void generate_posts_iterator::increment()
parsing_context.get_current().journal = session.journal.get();
parsing_context.get_current().scope = &session;

if (session.journal->read(parsing_context) != 0) {
if (session.journal->read(parsing_context, NO_HASHES) != 0) {
VERIFY(session.journal->xacts.back()->valid());
posts.reset(*session.journal->xacts.back());
post = *posts++;
Expand Down
2 changes: 1 addition & 1 deletion src/global.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ void global_scope_t::parse_init(path init_file)
parsing_context.get_current().journal = session().journal.get();
parsing_context.get_current().scope = &report();

if (session().journal->read(parsing_context) > 0 ||
if (session().journal->read(parsing_context, NO_HASHES) > 0 ||
session().journal->auto_xacts.size() > 0 ||
session().journal->period_xacts.size() > 0) {
throw_(parse_error, _f("Transactions found in initialization file '%1%'")
Expand Down
6 changes: 5 additions & 1 deletion src/item.cc
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,17 @@ item_t::set_tag(const string& tag,

string_map::iterator i = metadata->find(tag);
if (i == metadata->end()) {
DEBUG("item.meta", "Setting new metadata value");
std::pair<string_map::iterator, bool> result
= metadata->insert(string_map::value_type(tag, tag_data_t(data, false)));
assert(result.second);
return result.first;
} else {
if (overwrite_existing)
DEBUG("item.meta", "Found old metadata value");
if (overwrite_existing) {
DEBUG("item.meta", "Overwriting old metadata value");
(*i).second = tag_data_t(data, false);
}
return i;
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/journal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,8 @@ bool journal_t::remove_xact(xact_t * xact)
return true;
}

std::size_t journal_t::read(parse_context_stack_t& context)
std::size_t journal_t::read(parse_context_stack_t& context,
hash_type_t hash_type)
{
std::size_t count = 0;
try {
Expand All @@ -485,7 +486,7 @@ std::size_t journal_t::read(parse_context_stack_t& context)
if (! current.master)
current.master = master;

count = read_textual(context);
count = read_textual(context, hash_type);
if (count > 0) {
if (! current.pathname.empty())
sources.push_back(fileinfo_t(current.pathname));
Expand Down
6 changes: 4 additions & 2 deletions src/journal.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ class journal_t : public noncopyable
return period_xacts.end();
}

std::size_t read(parse_context_stack_t& context);
std::size_t read(parse_context_stack_t& context,
hash_type_t hash_type);

bool has_xdata();
void clear_xdata();
Expand All @@ -193,7 +194,8 @@ class journal_t : public noncopyable

private:

std::size_t read_textual(parse_context_stack_t& context);
std::size_t read_textual(parse_context_stack_t& context,
hash_type_t hash_type);

bool should_check_payees();
bool payee_not_registered(const string& name);
Expand Down
2 changes: 1 addition & 1 deletion src/precmd.cc
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ namespace {
parsing_context.get_current().journal = report.session.journal.get();
parsing_context.get_current().scope = &report.session;

report.session.journal->read(parsing_context);
report.session.journal->read(parsing_context, NO_HASHES);
report.session.journal->clear_xdata();
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/session.cc
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ std::size_t session_t::read_data(const string& master_account)
parsing_context.push(*price_db_path);
parsing_context.get_current().journal = journal.get();
try {
if (journal->read(parsing_context) > 0)
if (journal->read(parsing_context, HANDLER(hashes_).hash_type) > 0)
throw_(parse_error, _("Transactions not allowed in price history file"));
}
catch (...) {
Expand Down Expand Up @@ -169,7 +169,7 @@ std::size_t session_t::read_data(const string& master_account)
parsing_context.get_current().journal = journal.get();
parsing_context.get_current().master = acct;
try {
xact_count += journal->read(parsing_context);
xact_count += journal->read(parsing_context, HANDLER(hashes_).hash_type);
}
catch (...) {
parsing_context.pop();
Expand Down Expand Up @@ -230,7 +230,7 @@ journal_t * session_t::read_journal_from_string(const string& data)
parsing_context.get_current().journal = journal.get();
parsing_context.get_current().master = journal->master;
try {
journal->read(parsing_context);
journal->read(parsing_context, HANDLER(hashes_).hash_type);
}
catch (...) {
parsing_context.pop();
Expand Down Expand Up @@ -331,6 +331,9 @@ option_t<session_t> * session_t::lookup_option(const char * p)
case 'f':
OPT_(file_); // -f
break;
case 'h':
OPT(hashes_);
break;
case 'i':
OPT(input_date_format_);
break;
Expand Down
15 changes: 15 additions & 0 deletions src/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class session_t : public symbol_scope_t
HANDLER(decimal_comma).report(out);
HANDLER(time_colon).report(out);
HANDLER(file_).report(out);
HANDLER(hashes_).report(out);
HANDLER(input_date_format_).report(out);
HANDLER(explicit).report(out);
HANDLER(master_account_).report(out);
Expand Down Expand Up @@ -162,6 +163,20 @@ class session_t : public symbol_scope_t
data_files.push_back(str);
});

OPTION__
(session_t, hashes_,
hash_type_t hash_type = NO_HASHES;
CTOR(session_t, hashes_) {}
DO_(str) {
if (str == "sha512" || str == "SHA512") {
hash_type = HASH_SHA512;
} else if (str == "sha512_256" || str == "SHA512_256") {
hash_type = HASH_SHA512_256;
} else {
throw_(std::invalid_argument, _f("Unrecognized hash type"));
}
});

OPTION_(session_t, input_date_format_, DO_(str) {
// This changes static variables inside times.h, which affects the
// basic date parser.
Expand Down
Loading
Loading