Skip to content

Commit

Permalink
q-dev: full identity
Browse files Browse the repository at this point in the history
better error message
update docs
  • Loading branch information
piotrbartman committed May 26, 2024
1 parent 160f50e commit ff0d883
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 29 deletions.
4 changes: 3 additions & 1 deletion doc/manpages/qvm-device.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ Synopsis
| :command:`qvm-device` *DEVICE_CLASS* {list,ls,l} [*options*] <*vm-name*>
| :command:`qvm-device` *DEVICE_CLASS* {attach,at,a} [*options*] <*vm-name*> <*device*>
| :command:`qvm-device` *DEVICE_CLASS* {detach,dt,d} [*options*] <*vm-name*> [<*device*>]
| :command:`qvm-device` *DEVICE_CLASS* {assign,s} [*options*] <*vm-name*> <*device*>
| :command:`qvm-device` *DEVICE_CLASS* {unassign,u} [*options*] <*vm-name*> [<*device*>]
| :command:`qvm-device` *DEVICE_CLASS* {info,i} [*options*] <*vm-name*> [<*device*>]
| :command:`qvm-*DEVICE_CLASS*` {list,ls,l,attach,at,a,detach,dt,d,info,i} [*options*] <*vmname*> ...
| :command:`qvm-*DEVICE_CLASS*` {list,ls,l,attach,at,a,detach,dt,d,assign,s,unassign,u,info,i} [*options*] <*vmname*> ...
.. note:: :command:`qvm-block`, :command:`qvm-usb` and :command:`qvm-pci` are just aliases for :command:`qvm-device block`, :command:`qvm-device usb` and :command:`qvm-device pci` respectively.

Expand Down
83 changes: 59 additions & 24 deletions qubesadmin/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def __lt__(self, other):
if isinstance(other, Device):
return (self.backend_domain, self.ident) < \
(other.backend_domain, other.ident)
return NotImplemented
return NotImplemented()

def __repr__(self):
return "[%s]:%s" % (self.backend_domain, self.ident)
Expand Down Expand Up @@ -160,9 +160,10 @@ def devclass(self, devclass: str):

class DeviceCategory(Enum):
"""
Category of peripheral device.
Arbitrarily selected interfaces that are important to users,
thus deserving special recognition such as a custom icon, etc.
"""
Other = "*******"

Expand Down Expand Up @@ -212,12 +213,16 @@ def from_str(interface_encoding: str) -> 'DeviceCategory':


class DeviceInterface:
"""
Peripheral device interface wrapper.
"""

def __init__(self, interface_encoding: str, devclass: Optional[str] = None):
ifc_padded = interface_encoding.ljust(6, '*')
if devclass:
if len(ifc_padded) > 6:
print(
f"interface_encoding is too long "
f"{interface_encoding=} is too long "
f"(is {len(interface_encoding)}, expected max. 6) "
f"for given {devclass=}",
file=sys.stderr
Expand All @@ -229,7 +234,7 @@ def __init__(self, interface_encoding: str, devclass: Optional[str] = None):
devclass = known_devclasses.get(interface_encoding[0], None)
if len(ifc_padded) > 7:
print(
f"interface_encoding is too long "
f"{interface_encoding=} is too long "
f"(is {len(interface_encoding)}, expected max. 7)",
file=sys.stderr
)
Expand Down Expand Up @@ -258,18 +263,26 @@ def unknown(cls) -> 'DeviceInterface':
""" Value for unknown device interface. """
return cls("?******")

@property
def __repr__(self):
return self._interface_encoding

@property
def __str__(self):
if self.devclass == "block":
return "Block device"
if self.devclass in ("usb", "pci"):
self._load_classes(self.devclass).get(
self._interface_encoding[1:],
f"Unclassified {self.devclass} device")
result = self._load_classes(self.devclass).get(
self._interface_encoding[1:], None)
if result is None:
result = self._load_classes(self.devclass).get(
self._interface_encoding[1:-2] + '**', None)
if result is None:
result = self._load_classes(self.devclass).get(
self._interface_encoding[1:-4] + '****', None)
if result is None:
result = f"Unclassified {self.devclass} device"
return result
if self.devclass == 'mic':
return "Microphone"
return repr(self)

@staticmethod
Expand Down Expand Up @@ -591,6 +604,27 @@ def _deserialize(
def frontend_domain(self):
return self.data.get("frontend_domain", None)

@property
def full_identity(self) -> str:
"""
Get user understandable identification of device not related to ports.
In addition to the description returns presented interfaces.
It is used to auto-attach usb devices, so an attacking device needs to
mimic not only a name, but also interfaces of trusted device (and have
to be plugged to the same port). For a common user it is all the data
she uses to recognize the device.
"""
allowed_chars = string.digits + string.ascii_letters + '-_.'
description = ""
for char in self.description:
if char in allowed_chars:
description += char
else:
description += "_"
interfaces = ''.join(repr(ifc) for ifc in self.interfaces)
return f'{description}:{interfaces}'


def serialize_str(value: str):
return repr(str(value))
Expand Down Expand Up @@ -771,7 +805,8 @@ def _deserialize(
allowed_chars_key = string.digits + string.ascii_letters + '-_.'
allowed_chars_value = allowed_chars_key + ',+:'

untrusted_decoded = untrusted_serialization.decode('ascii', 'strict')
untrusted_decoded = untrusted_serialization.decode(
'ascii', 'strict').strip()
keys = []
values = []
untrusted_key, _, untrusted_rest = untrusted_decoded.partition("='")
Expand Down Expand Up @@ -808,7 +843,7 @@ def _deserialize(

if properties['backend_domain'] != expected_backend_domain.name:
raise UnexpectedDeviceProperty(
f"Got device exposed by {properties['backend_domain']}"
f"Got device exposed by {properties['backend_domain']} "
f"when expected devices from {expected_backend_domain.name}.")
properties['backend_domain'] = expected_backend_domain

Expand Down Expand Up @@ -895,31 +930,31 @@ def detach(self, device_assignment: DeviceAssignment) -> None:
device_assignment.backend_domain,
device_assignment.ident))

def assign(self, device_assignment: DeviceAssignment) -> None:
def assign(self, assignment: DeviceAssignment) -> None:
"""
Assign device to domain (add to :file:`qubes.xml`).
:param DeviceAssignment device_assignment: device object
:param DeviceAssignment assignment: device object
"""

if not device_assignment.frontend_domain:
device_assignment.frontend_domain = self._vm
if not assignment.frontend_domain:
assignment.frontend_domain = self._vm
else:
assert device_assignment.frontend_domain == self._vm, \
assert assignment.frontend_domain == self._vm, \
"Trying to assign DeviceAssignment belonging to other domain"
if not device_assignment.devclass_is_set:
device_assignment.devclass = self._class
elif device_assignment.devclass != self._class:
raise ValueError(
if not assignment.devclass_is_set:
assignment.devclass = self._class
elif assignment.devclass != self._class:
raise qubesadmin.ext.QubesValueError(
f"Device assignment class does not match to expected: "
f"{device_assignment.devclass=}!={self._class=}")
f"{assignment.devclass=}!={self._class=}")

self._vm.qubesd_call(None,
'admin.vm.device.{}.Assign'.format(self._class),
'{!s}+{!s}'.format(
device_assignment.backend_domain,
device_assignment.ident),
device_assignment.serialize())
assignment.backend_domain,
assignment.ident),
assignment.serialize())

def unassign(self, device_assignment: DeviceAssignment) -> None:
"""
Expand Down
10 changes: 6 additions & 4 deletions qubesadmin/tools/qvm_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def assign_device(args):
options = dict(opt.split('=', 1) for opt in args.option or [])
if args.ro:
options['read-only'] = 'yes'
if device.devclass == 'usb':
options['identity'] = device.full_identity
device_assignment.attach_automatically = True
device_assignment.required = args.required
device_assignment.options = options
Expand Down Expand Up @@ -266,10 +268,10 @@ def parse_qubes_app(self, parser, namespace):
raise KeyError(device_id)
except KeyError:
parser.error_runtime(
"backend vm {!r} doesn't expose device {!r}".format(
vmname, device_id))
device = qubesadmin.devices.Device(vm, device_id)
setattr(namespace, self.dest, device)
f"backend vm {vmname!r} doesn't expose "
f"{devclass} device {device_id!r}")
dev = qubesadmin.devices.Device(vm, device_id, devclass)
setattr(namespace, self.dest, dev)
except ValueError:
parser.error(
'expected a backend vm & device id combination like foo:bar '
Expand Down

0 comments on commit ff0d883

Please sign in to comment.