2020-06-25 14:55:48 +02:00
|
|
|
#!/usr/bin/env python3
|
2021-01-16 14:44:19 +01:00
|
|
|
# group: rw
|
2020-06-25 14:55:48 +02:00
|
|
|
#
|
|
|
|
# Test case for encryption key management versus image sharing
|
|
|
|
#
|
|
|
|
# Copyright (C) 2019 Red Hat, Inc.
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
|
|
|
|
import iotests
|
|
|
|
import os
|
|
|
|
import time
|
|
|
|
import json
|
|
|
|
|
|
|
|
test_img = os.path.join(iotests.test_dir, 'test.img')
|
|
|
|
|
|
|
|
class Secret:
|
|
|
|
def __init__(self, index):
|
|
|
|
self._id = "keysec" + str(index)
|
|
|
|
# you are not supposed to see the password...
|
|
|
|
self._secret = "hunter" + str(index)
|
|
|
|
|
|
|
|
def id(self):
|
|
|
|
return self._id
|
|
|
|
|
|
|
|
def secret(self):
|
|
|
|
return self._secret
|
|
|
|
|
|
|
|
def to_cmdline_object(self):
|
|
|
|
return [ "secret,id=" + self._id + ",data=" + self._secret]
|
|
|
|
|
|
|
|
def to_qmp_object(self):
|
|
|
|
return { "qom_type" : "secret", "id": self.id(),
|
2021-02-22 12:57:37 +01:00
|
|
|
"data": self.secret() }
|
2020-06-25 14:55:48 +02:00
|
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
|
|
class EncryptionSetupTestCase(iotests.QMPTestCase):
|
|
|
|
|
|
|
|
# test case startup
|
|
|
|
def setUp(self):
|
|
|
|
|
|
|
|
# start the VMs
|
|
|
|
self.vm1 = iotests.VM(path_suffix = 'VM1')
|
|
|
|
self.vm2 = iotests.VM(path_suffix = 'VM2')
|
|
|
|
self.vm1.launch()
|
|
|
|
self.vm2.launch()
|
|
|
|
|
|
|
|
# create the secrets and load 'em into the VMs
|
|
|
|
self.secrets = [ Secret(i) for i in range(0, 4) ]
|
|
|
|
for secret in self.secrets:
|
|
|
|
result = self.vm1.qmp("object-add", **secret.to_qmp_object())
|
|
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm2.qmp("object-add", **secret.to_qmp_object())
|
|
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
|
|
|
|
# test case shutdown
|
|
|
|
def tearDown(self):
|
|
|
|
# stop the VM
|
|
|
|
self.vm1.shutdown()
|
|
|
|
self.vm2.shutdown()
|
|
|
|
|
|
|
|
###########################################################################
|
|
|
|
# create the encrypted block device using qemu-img
|
|
|
|
def createImg(self, file, secret):
|
|
|
|
|
|
|
|
output = iotests.qemu_img_pipe(
|
|
|
|
'create',
|
|
|
|
'--object', *secret.to_cmdline_object(),
|
|
|
|
'-f', iotests.imgfmt,
|
|
|
|
'-o', 'key-secret=' + secret.id(),
|
|
|
|
'-o', 'iter-time=10',
|
|
|
|
file,
|
|
|
|
'1M')
|
|
|
|
|
|
|
|
iotests.log(output, filters=[iotests.filter_test_dir])
|
|
|
|
|
|
|
|
# attempts to add a key using qemu-img
|
|
|
|
def addKey(self, file, secret, new_secret):
|
|
|
|
|
|
|
|
image_options = {
|
|
|
|
'key-secret' : secret.id(),
|
|
|
|
'driver' : iotests.imgfmt,
|
|
|
|
'file' : {
|
|
|
|
'driver':'file',
|
|
|
|
'filename': file,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
output = iotests.qemu_img_pipe(
|
|
|
|
'amend',
|
|
|
|
'--object', *secret.to_cmdline_object(),
|
|
|
|
'--object', *new_secret.to_cmdline_object(),
|
|
|
|
|
|
|
|
'-o', 'state=active',
|
|
|
|
'-o', 'new-secret=' + new_secret.id(),
|
|
|
|
'-o', 'iter-time=10',
|
|
|
|
|
|
|
|
"json:" + json.dumps(image_options)
|
|
|
|
)
|
|
|
|
|
|
|
|
iotests.log(output, filters=[iotests.filter_test_dir])
|
|
|
|
|
|
|
|
###########################################################################
|
|
|
|
# open an encrypted block device
|
|
|
|
def openImageQmp(self, vm, id, file, secret,
|
|
|
|
readOnly = False, reOpen = False):
|
|
|
|
|
2021-07-08 13:47:09 +02:00
|
|
|
command = 'blockdev-reopen' if reOpen else 'blockdev-add'
|
2020-06-25 14:55:48 +02:00
|
|
|
|
2021-07-08 13:47:07 +02:00
|
|
|
opts = {
|
2020-06-25 14:55:48 +02:00
|
|
|
'driver': iotests.imgfmt,
|
|
|
|
'node-name': id,
|
|
|
|
'read-only': readOnly,
|
|
|
|
'key-secret' : secret.id(),
|
|
|
|
'file': {
|
|
|
|
'driver': 'file',
|
|
|
|
'filename': test_img,
|
|
|
|
}
|
|
|
|
}
|
2021-07-08 13:47:07 +02:00
|
|
|
|
|
|
|
if reOpen:
|
|
|
|
result = vm.qmp(command, options=[opts])
|
|
|
|
else:
|
|
|
|
result = vm.qmp(command, **opts)
|
2020-06-25 14:55:48 +02:00
|
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
|
2020-07-19 14:20:59 +02:00
|
|
|
|
|
|
|
###########################################################################
|
|
|
|
# add virtio-blk consumer for a block device
|
|
|
|
def addImageUser(self, vm, id, disk_id, share_rw=False):
|
|
|
|
result = vm.qmp('device_add', **
|
|
|
|
{
|
|
|
|
'driver': 'virtio-blk',
|
|
|
|
'id': id,
|
|
|
|
'drive': disk_id,
|
|
|
|
'share-rw' : share_rw
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
iotests.log(result)
|
|
|
|
|
2020-06-25 14:55:48 +02:00
|
|
|
# close the encrypted block device
|
|
|
|
def closeImageQmp(self, vm, id):
|
|
|
|
result = vm.qmp('blockdev-del', **{ 'node-name': id })
|
|
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
|
|
|
|
###########################################################################
|
|
|
|
|
|
|
|
# add a key to an encrypted block device
|
|
|
|
def addKeyQmp(self, vm, id, new_secret):
|
|
|
|
|
|
|
|
args = {
|
|
|
|
'node-name': id,
|
|
|
|
'job-id' : 'job0',
|
|
|
|
'options' : {
|
|
|
|
'state' : 'active',
|
|
|
|
'driver' : iotests.imgfmt,
|
|
|
|
'new-secret': new_secret.id(),
|
|
|
|
'iter-time' : 10
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
result = vm.qmp('x-blockdev-amend', **args)
|
|
|
|
assert result['return'] == {}
|
|
|
|
vm.run_job('job0')
|
|
|
|
|
|
|
|
# test that when the image opened by two qemu processes,
|
2020-07-19 14:20:59 +02:00
|
|
|
# neither of them can update the encryption keys
|
2020-06-25 14:55:48 +02:00
|
|
|
def test1(self):
|
|
|
|
self.createImg(test_img, self.secrets[0]);
|
|
|
|
|
|
|
|
# VM1 opens the image and adds a key
|
|
|
|
self.openImageQmp(self.vm1, "testdev", test_img, self.secrets[0])
|
|
|
|
self.addKeyQmp(self.vm1, "testdev", new_secret = self.secrets[1])
|
|
|
|
|
|
|
|
|
|
|
|
# VM2 opens the image
|
|
|
|
self.openImageQmp(self.vm2, "testdev", test_img, self.secrets[0])
|
|
|
|
|
|
|
|
|
|
|
|
# neither VMs now should be able to add a key
|
|
|
|
self.addKeyQmp(self.vm1, "testdev", new_secret = self.secrets[2])
|
|
|
|
self.addKeyQmp(self.vm2, "testdev", new_secret = self.secrets[2])
|
|
|
|
|
|
|
|
|
|
|
|
# VM 1 closes the image
|
|
|
|
self.closeImageQmp(self.vm1, "testdev")
|
|
|
|
|
|
|
|
|
|
|
|
# now VM2 can add the key
|
|
|
|
self.addKeyQmp(self.vm2, "testdev", new_secret = self.secrets[2])
|
|
|
|
|
|
|
|
|
|
|
|
# qemu-img should also not be able to add a key
|
|
|
|
self.addKey(test_img, self.secrets[0], self.secrets[2])
|
|
|
|
|
|
|
|
# cleanup
|
|
|
|
self.closeImageQmp(self.vm2, "testdev")
|
|
|
|
os.remove(test_img)
|
|
|
|
|
|
|
|
|
2020-07-19 14:20:59 +02:00
|
|
|
# test that when the image opened by two qemu processes,
|
|
|
|
# even if first VM opens it read-only, the second can't update encryption
|
|
|
|
# keys
|
2020-06-25 14:55:48 +02:00
|
|
|
def test2(self):
|
|
|
|
self.createImg(test_img, self.secrets[0]);
|
|
|
|
|
|
|
|
# VM1 opens the image readonly
|
|
|
|
self.openImageQmp(self.vm1, "testdev", test_img, self.secrets[0],
|
|
|
|
readOnly = True)
|
|
|
|
|
|
|
|
# VM2 opens the image
|
|
|
|
self.openImageQmp(self.vm2, "testdev", test_img, self.secrets[0])
|
|
|
|
|
|
|
|
# VM1 can't add a key since image is readonly
|
|
|
|
self.addKeyQmp(self.vm1, "testdev", new_secret = self.secrets[2])
|
|
|
|
|
|
|
|
# VM2 can't add a key since VM is has the image opened
|
|
|
|
self.addKeyQmp(self.vm2, "testdev", new_secret = self.secrets[2])
|
|
|
|
|
|
|
|
|
|
|
|
#VM1 reopens the image read-write
|
|
|
|
self.openImageQmp(self.vm1, "testdev", test_img, self.secrets[0],
|
|
|
|
reOpen = True, readOnly = False)
|
|
|
|
|
|
|
|
# VM1 still can't add the key
|
|
|
|
self.addKeyQmp(self.vm1, "testdev", new_secret = self.secrets[2])
|
|
|
|
|
|
|
|
# VM2 gets away
|
|
|
|
self.closeImageQmp(self.vm2, "testdev")
|
|
|
|
|
|
|
|
# VM1 now can add the key
|
|
|
|
self.addKeyQmp(self.vm1, "testdev", new_secret = self.secrets[2])
|
|
|
|
|
|
|
|
self.closeImageQmp(self.vm1, "testdev")
|
|
|
|
os.remove(test_img)
|
|
|
|
|
2020-07-19 14:20:59 +02:00
|
|
|
# test that two VMs can't open the same luks image by default
|
|
|
|
# and attach it to a guest device
|
|
|
|
def test3(self):
|
|
|
|
self.createImg(test_img, self.secrets[0]);
|
|
|
|
|
|
|
|
self.openImageQmp(self.vm1, "testdev", test_img, self.secrets[0])
|
|
|
|
self.addImageUser(self.vm1, "testctrl", "testdev")
|
|
|
|
|
|
|
|
self.openImageQmp(self.vm2, "testdev", test_img, self.secrets[0])
|
|
|
|
self.addImageUser(self.vm2, "testctrl", "testdev")
|
|
|
|
|
|
|
|
|
|
|
|
# test that two VMs can attach the same luks image to a guest device,
|
|
|
|
# if both use share-rw=on
|
|
|
|
def test4(self):
|
|
|
|
self.createImg(test_img, self.secrets[0]);
|
|
|
|
|
|
|
|
self.openImageQmp(self.vm1, "testdev", test_img, self.secrets[0])
|
|
|
|
self.addImageUser(self.vm1, "testctrl", "testdev", share_rw=True)
|
|
|
|
|
|
|
|
self.openImageQmp(self.vm2, "testdev", test_img, self.secrets[0])
|
|
|
|
self.addImageUser(self.vm2, "testctrl", "testdev", share_rw=True)
|
|
|
|
|
|
|
|
|
2020-06-25 14:55:48 +02:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
# support only raw luks since luks encrypted qcow2 is a proper
|
|
|
|
# format driver which doesn't allow any sharing
|
|
|
|
iotests.activate_logging()
|
|
|
|
iotests.main(supported_fmts = ['luks'])
|