diff --git a/borg/archiver.py b/borg/archiver.py index 9206a4b9d..8a243f890 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -48,6 +48,8 @@ def argument(args, str_or_bool): """If bool is passed, return it. If str is passed, retrieve named attribute from args.""" if isinstance(str_or_bool, str): return getattr(args, str_or_bool) + if isinstance(str_or_bool, (list, tuple)): + return any(getattr(args, item) for item in str_or_bool) return str_or_bool @@ -721,29 +723,43 @@ class Archiver: DASHES) return self.exit_code - @with_repository(fake='tam', invert_fake=True, manifest=False, exclusive=True) + @with_repository(fake=('tam', 'disable_tam'), invert_fake=True, manifest=False, exclusive=True) def do_upgrade(self, args, repository, manifest=None, key=None): """upgrade a repository from a previous version""" if args.tam: manifest, key = Manifest.load(repository, force_tam_not_required=args.force) - if not manifest.tam_verified: + if not manifest.tam_verified or not manifest.config.get(b'tam_required', False): # The standard archive listing doesn't include the archive ID like in borg 1.1.x print('Manifest contents:') for archive_info in manifest.list_archive_infos(sort_by='ts'): print(format_archive(archive_info), '[%s]' % bin_to_hex(archive_info.id)) + manifest.config[b'tam_required'] = True manifest.write() repository.commit() if not key.tam_required: key.tam_required = True key.change_passphrase(key._passphrase) - print('Updated key') + print('Key updated') if hasattr(key, 'find_key'): print('Key location:', key.find_key()) if not tam_required(repository): tam_file = tam_required_file(repository) open(tam_file, 'w').close() print('Updated security database') + elif args.disable_tam: + manifest, key = Manifest.load(repository, force_tam_not_required=True) + if tam_required(repository): + os.unlink(tam_required_file(repository)) + if key.tam_required: + key.tam_required = False + key.change_passphrase(key._passphrase) + print('Key updated') + if hasattr(key, 'find_key'): + print('Key location:', key.find_key()) + manifest.config[b'tam_required'] = False + manifest.write() + repository.commit() else: # mainly for upgrades from Attic repositories, # but also supports borg 0.xx -> 1.0 upgrade. @@ -1666,6 +1682,10 @@ class Archiver: If a repository is accidentally modified with a pre-1.0.9 client after this upgrade, use ``borg upgrade --tam --force REPO`` to remedy it. + If you routinely do this you might not want to enable this upgrade + (which will leave you exposed to the security issue). You can + reverse the upgrade by issuing ``borg upgrade --disable-tam REPO``. + See https://borgbackup.readthedocs.io/en/stable/changes.html#pre-1-0-9-manifest-spoofing-vulnerability for details. @@ -1729,6 +1749,8 @@ class Archiver: help="""Force upgrade""") subparser.add_argument('--tam', dest='tam', action='store_true', help="""Enable manifest authentication (in key and cache) (Borg 1.0.9 and later)""") + subparser.add_argument('--disable-tam', dest='disable_tam', action='store_true', + help="""Disable manifest authentication (in key and cache)""") subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False), help='path to the repository to be upgraded') diff --git a/borg/helpers.py b/borg/helpers.py index df5a213af..b38ec9455 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -129,10 +129,17 @@ class Manifest: manifest.config = m[b'config'] # valid item keys are whatever is known in the repo or every key we know manifest.item_keys = frozenset(m.get(b'item_keys', [])) | ITEM_KEYS - if manifest.config.get(b'tam_required', False) and manifest.tam_verified and not tam_required(repository): - logger.debug('Manifest is TAM verified and says TAM is required, updating security database...') - file = tam_required_file(repository) - open(file, 'w').close() + + if manifest.tam_verified: + manifest_required = manifest.config.get(b'tam_required', False) + security_required = tam_required(repository) + if manifest_required and not security_required: + logger.debug('Manifest is TAM verified and says TAM is required, updating security database...') + file = tam_required_file(repository) + open(file, 'w').close() + if not manifest_required and security_required: + logger.debug('Manifest is TAM verified and says TAM is *not* required, updating security database...') + os.unlink(tam_required_file(repository)) return manifest, key def write(self): diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 5af3e3931..6968ec33b 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1534,6 +1534,17 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase): class ManifestAuthenticationTest(ArchiverTestCaseBase): + def spoof_manifest(self, repository): + with repository: + _, key = Manifest.load(repository) + repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ + 'version': 1, + 'archives': {}, + 'config': {}, + 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), + }))) + repository.commit() + def test_fresh_init_tam_required(self): self.cmd('init', self.repository_location) repository = Repository(self.repository_path, exclusive=True) @@ -1573,15 +1584,7 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): assert 'archive1234' in output assert 'TAM-verified manifest' in output # Try to spoof / modify pre-1.0.9 - with repository: - _, key = Manifest.load(repository) - repository.put(Manifest.MANIFEST_ID, key.encrypt(msgpack.packb({ - 'version': 1, - 'archives': {}, - 'config': {}, - 'timestamp': (datetime.utcnow() + timedelta(days=1)).isoformat(), - }))) - repository.commit() + self.spoof_manifest(repository) # Fails with pytest.raises(TAMRequiredError): self.cmd('list', self.repository_location) @@ -1589,6 +1592,22 @@ class ManifestAuthenticationTest(ArchiverTestCaseBase): self.cmd('upgrade', '--tam', '--force', self.repository_location) self.cmd('list', self.repository_location) + def test_disable(self): + self.cmd('init', self.repository_location) + self.create_src_archive('archive1234') + self.cmd('upgrade', '--disable-tam', self.repository_location) + repository = Repository(self.repository_path, exclusive=True) + self.spoof_manifest(repository) + assert not self.cmd('list', self.repository_location) + + def test_disable2(self): + self.cmd('init', self.repository_location) + self.create_src_archive('archive1234') + repository = Repository(self.repository_path, exclusive=True) + self.spoof_manifest(repository) + self.cmd('upgrade', '--disable-tam', self.repository_location) + assert not self.cmd('list', self.repository_location) + @pytest.mark.skipif(sys.platform == 'cygwin', reason='remote is broken on cygwin and hangs') class RemoteArchiverTestCase(ArchiverTestCase):