Skip to content

Commit

Permalink
check: rebuild_manifest must verify archive TAM
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasWaldmann committed Aug 29, 2023
1 parent 6aa350a commit a2ee13f
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 14 deletions.
13 changes: 13 additions & 0 deletions src/borg/archive.py
Expand Up @@ -1992,6 +1992,19 @@ def valid_archive(obj):
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)
Expand Down
58 changes: 58 additions & 0 deletions src/borg/crypto/key.py
Expand Up @@ -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
Expand All @@ -81,6 +90,15 @@ def __init__(self):
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."""

Expand Down Expand Up @@ -279,6 +297,46 @@ def unpack_and_verify_manifest(self, data, force_tam_not_required=False):
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
Expand Down
4 changes: 2 additions & 2 deletions src/borg/helpers/msgpack.py
Expand Up @@ -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)


Expand Down
22 changes: 10 additions & 12 deletions src/borg/testsuite/archiver/check_cmd.py
Expand Up @@ -6,7 +6,6 @@
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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit a2ee13f

Please sign in to comment.