mirror of
https://github.com/borgbackup/borg.git
synced 2025-03-10 06:03:38 +00:00
check: rebuild_manifest must verify archive TAM
This commit is contained in:
parent
8ae06199ef
commit
1fd94bd38f
4 changed files with 78 additions and 2 deletions
|
@ -1813,6 +1813,19 @@ class ArchiveChecker:
|
||||||
except msgpack.UnpackException:
|
except msgpack.UnpackException:
|
||||||
continue
|
continue
|
||||||
if valid_archive(archive):
|
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)
|
archive = ArchiveItem(internal_dict=archive)
|
||||||
name = archive.name
|
name = archive.name
|
||||||
logger.info('Found archive %s', name)
|
logger.info('Found archive %s', name)
|
||||||
|
|
|
@ -89,6 +89,13 @@ class TAMRequiredError(IntegrityError):
|
||||||
traceback = False
|
traceback = False
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveTAMRequiredError(TAMRequiredError):
|
||||||
|
__doc__ = textwrap.dedent("""
|
||||||
|
Archive '{}' is unauthenticated, but it is required for this repository.
|
||||||
|
""").strip()
|
||||||
|
traceback = False
|
||||||
|
|
||||||
|
|
||||||
class TAMInvalid(IntegrityError):
|
class TAMInvalid(IntegrityError):
|
||||||
__doc__ = IntegrityError.__doc__
|
__doc__ = IntegrityError.__doc__
|
||||||
traceback = False
|
traceback = False
|
||||||
|
@ -98,6 +105,15 @@ class TAMInvalid(IntegrityError):
|
||||||
super().__init__('Manifest authentication did not verify')
|
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):
|
class TAMUnsupportedSuiteError(IntegrityError):
|
||||||
"""Could not verify manifest: Unsupported suite {!r}; a newer version is needed."""
|
"""Could not verify manifest: Unsupported suite {!r}; a newer version is needed."""
|
||||||
traceback = False
|
traceback = False
|
||||||
|
@ -266,6 +282,46 @@ class KeyBase:
|
||||||
logger.debug('TAM-verified manifest')
|
logger.debug('TAM-verified manifest')
|
||||||
return unpacked, True
|
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):
|
class PlaintextKey(KeyBase):
|
||||||
TYPE = 0x02
|
TYPE = 0x02
|
||||||
|
|
|
@ -209,6 +209,12 @@ def get_limited_unpacker(kind):
|
||||||
max_str_len=255, # archive name
|
max_str_len=255, # archive name
|
||||||
object_hook=StableDict,
|
object_hook=StableDict,
|
||||||
))
|
))
|
||||||
|
elif kind == 'archive':
|
||||||
|
args.update(dict(use_list=True, # default value
|
||||||
|
max_map_len=100, # ARCHIVE_KEYS ~= 20
|
||||||
|
max_str_len=10000, # comment
|
||||||
|
object_hook=StableDict,
|
||||||
|
))
|
||||||
elif kind == 'key':
|
elif kind == 'key':
|
||||||
args.update(dict(use_list=True, # default value
|
args.update(dict(use_list=True, # default value
|
||||||
max_array_len=0, # not used
|
max_array_len=0, # not used
|
||||||
|
|
|
@ -3957,7 +3957,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
|
||||||
corrupted_manifest = manifest + b'corrupted!'
|
corrupted_manifest = manifest + b'corrupted!'
|
||||||
repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
|
repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
|
||||||
|
|
||||||
archive = msgpack.packb({
|
archive_dict = {
|
||||||
'cmdline': [],
|
'cmdline': [],
|
||||||
'items': [],
|
'items': [],
|
||||||
'hostname': 'foo',
|
'hostname': 'foo',
|
||||||
|
@ -3965,7 +3965,8 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
|
||||||
'name': 'archive1',
|
'name': 'archive1',
|
||||||
'time': '2016-12-15T18:49:51.849711',
|
'time': '2016-12-15T18:49:51.849711',
|
||||||
'version': 1,
|
'version': 1,
|
||||||
})
|
}
|
||||||
|
archive = key.pack_and_authenticate_metadata(archive_dict, context=b'archive')
|
||||||
archive_id = key.id_hash(archive)
|
archive_id = key.id_hash(archive)
|
||||||
repository.put(archive_id, key.encrypt(archive))
|
repository.put(archive_id, key.encrypt(archive))
|
||||||
repository.commit(compact=False)
|
repository.commit(compact=False)
|
||||||
|
|
Loading…
Add table
Reference in a new issue