summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorThomas Waldmann <tw@waldmann-edv.de>2023-06-13 20:17:01 +0200
committerThomas Waldmann <tw@waldmann-edv.de>2023-08-29 21:10:32 +0200
commita2ee13fd341dcd004b4a06b17d6f2fc759327861 (patch)
tree5d2fe699afbbc060a2890566f3037dd1f73337c7 /src
parent6aa350aeb46ccaf2ce9f4a16a84e08614e02eec0 (diff)
check: rebuild_manifest must verify archive TAM
Diffstat (limited to 'src')
-rw-r--r--src/borg/archive.py13
-rw-r--r--src/borg/crypto/key.py58
-rw-r--r--src/borg/helpers/msgpack.py4
-rw-r--r--src/borg/testsuite/archiver/check_cmd.py22
4 files changed, 83 insertions, 14 deletions
diff --git a/src/borg/archive.py b/src/borg/archive.py
index 808f901fb..ef4217a9b 100644
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -1992,6 +1992,19 @@ class ArchiveChecker:
except msgpack.UnpackException:
continue
if valid_archive(archive):
+ # **after** doing the low-level checks and having a strong indication that we
+ # are likely looking at an archive item here, also check the TAM authentication:
+ try:
+ archive, verified = self.key.unpack_and_verify_archive(data, force_tam_not_required=False)
+ except IntegrityError:
+ # TAM issues - do not accept this archive!
+ # either somebody is trying to attack us with a fake archive data or
+ # we have an ancient archive made before TAM was a thing (borg < 1.0.9) **and** this repo
+ # was not correctly upgraded to borg 1.2.5 (see advisory at top of the changelog).
+ # borg can't tell the difference, so it has to assume this archive might be an attack
+ # and drops this archive.
+ continue
+ # note: if we get here and verified is False, a TAM is not required.
archive = ArchiveItem(internal_dict=archive)
name = archive.name
logger.info("Found archive %s", name)
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
index 338888b3a..ebaee58a2 100644
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -72,6 +72,15 @@ class TAMRequiredError(IntegrityError):
traceback = False
+class ArchiveTAMRequiredError(TAMRequiredError):
+ __doc__ = textwrap.dedent(
+ """
+ Archive '{}' is unauthenticated, but it is required for this repository.
+ """
+ ).strip()
+ traceback = False
+
+
class TAMInvalid(IntegrityError):
__doc__ = IntegrityError.__doc__
traceback = False
@@ -81,6 +90,15 @@ class TAMInvalid(IntegrityError):
super().__init__("Manifest authentication did not verify")
+class ArchiveTAMInvalid(IntegrityError):
+ __doc__ = IntegrityError.__doc__
+ traceback = False
+
+ def __init__(self):
+ # Error message becomes: "Data integrity error: Archive authentication did not verify"
+ super().__init__("Archive authentication did not verify")
+
+
class TAMUnsupportedSuiteError(IntegrityError):
"""Could not verify manifest: Unsupported suite {!r}; a newer version is needed."""
@@ -279,6 +297,46 @@ class KeyBase:
logger.debug("TAM-verified manifest")
return unpacked, True
+ def unpack_and_verify_archive(self, data, force_tam_not_required=False):
+ """Unpack msgpacked *data* and return (object, did_verify)."""
+ tam_required = self.tam_required
+ if force_tam_not_required and tam_required:
+ logger.warning("Archive authentication DISABLED.")
+ tam_required = False
+ data = bytearray(data)
+ unpacker = get_limited_unpacker("archive")
+ unpacker.feed(data)
+ unpacked = unpacker.unpack()
+ if b"tam" not in unpacked:
+ if tam_required:
+ archive_name = unpacked.get(b"name", b"<unknown>").decode("ascii", "replace")
+ raise ArchiveTAMRequiredError(archive_name)
+ else:
+ logger.debug("TAM not found and not required")
+ return unpacked, False
+ tam = unpacked.pop(b"tam", None)
+ if not isinstance(tam, dict):
+ raise ArchiveTAMInvalid()
+ tam_type = tam.get(b"type", b"<none>").decode("ascii", "replace")
+ if tam_type != "HKDF_HMAC_SHA512":
+ if tam_required:
+ raise TAMUnsupportedSuiteError(repr(tam_type))
+ else:
+ logger.debug("Ignoring TAM made with unsupported suite, since TAM is not required: %r", tam_type)
+ return unpacked, False
+ tam_hmac = tam.get(b"hmac")
+ tam_salt = tam.get(b"salt")
+ if not isinstance(tam_salt, bytes) or not isinstance(tam_hmac, bytes):
+ raise ArchiveTAMInvalid()
+ offset = data.index(tam_hmac)
+ data[offset : offset + 64] = bytes(64)
+ tam_key = self._tam_key(tam_salt, context=b"archive")
+ calculated_hmac = hmac.digest(tam_key, data, "sha512")
+ if not hmac.compare_digest(calculated_hmac, tam_hmac):
+ raise ArchiveTAMInvalid()
+ logger.debug("TAM-verified archive")
+ return unpacked, True
+
class PlaintextKey(KeyBase):
TYPE = KeyType.PLAINTEXT
diff --git a/src/borg/helpers/msgpack.py b/src/borg/helpers/msgpack.py
index ddbd95944..dce6eb33f 100644
--- a/src/borg/helpers/msgpack.py
+++ b/src/borg/helpers/msgpack.py
@@ -219,10 +219,10 @@ def get_limited_unpacker(kind):
args = dict(use_list=False, max_buffer_size=3 * max(BUFSIZE, MAX_OBJECT_SIZE)) # return tuples, not lists
if kind in ("server", "client"):
pass # nothing special
- elif kind in ("manifest", "key"):
+ elif kind in ("manifest", "archive", "key"):
args.update(dict(use_list=True, object_hook=StableDict)) # default value
else:
- raise ValueError('kind must be "server", "client", "manifest" or "key"')
+ raise ValueError('kind must be "server", "client", "manifest", "archive" or "key"')
return Unpacker(**args)
diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py
index b5db32538..5ddce3437 100644
--- a/src/borg/testsuite/archiver/check_cmd.py
+++ b/src/borg/testsuite/archiver/check_cmd.py
@@ -6,7 +6,6 @@ import pytest
from ...archive import ChunkBuffer
from ...constants import * # NOQA
from ...helpers import bin_to_hex
-from ...helpers import msgpack
from ...manifest import Manifest
from ...repository import Repository
from . import cmd, src_file, create_src_archive, open_archive, generate_archiver_tests, RK_ENCRYPTION
@@ -233,17 +232,16 @@ def test_manifest_rebuild_duplicate_archive(archivers, request):
manifest = repository.get(Manifest.MANIFEST_ID)
corrupted_manifest = manifest + b"corrupted!"
repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
- archive = msgpack.packb(
- {
- "command_line": "",
- "item_ptrs": [],
- "hostname": "foo",
- "username": "bar",
- "name": "archive1",
- "time": "2016-12-15T18:49:51.849711",
- "version": 2,
- }
- )
+ archive_dict = {
+ "command_line": "",
+ "item_ptrs": [],
+ "hostname": "foo",
+ "username": "bar",
+ "name": "archive1",
+ "time": "2016-12-15T18:49:51.849711",
+ "version": 2,
+ }
+ archive = repo_objs.key.pack_and_authenticate_metadata(archive_dict, context=b"archive")
archive_id = repo_objs.id_hash(archive)
repository.put(archive_id, repo_objs.format(archive_id, {}, archive))
repository.commit(compact=False)