Преглед изворни кода

check: rebuild_manifest must verify archive TAM

Thomas Waldmann пре 2 година
родитељ
комит
a2ee13fd34
4 измењених фајлова са 83 додато и 14 уклоњено
  1. 13 0
      src/borg/archive.py
  2. 58 0
      src/borg/crypto/key.py
  3. 2 2
      src/borg/helpers/msgpack.py
  4. 10 12
      src/borg/testsuite/archiver/check_cmd.py

+ 13 - 0
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)

+ 58 - 0
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

+ 2 - 2
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)
 
 

+ 10 - 12
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)