qubes.api – API

API sanitization is very important to maintain the AdminVM and everything it controls, secure.

Simple rules:

  • Handle exceptions gracefully. Unhandled exceptions have the traceback only in the AdminVM, not allowing clients to know what happened. More information about exceptions can be found at qubes.exc.

  • Failure to serve a request must raise exceptions that can be understood by the client. Each stage below allows a different class of exception to be shown. Be careful to not reveal more information than what the client is allowed to know at each stage.

Stages:

.# Sanitize data

  • Must never print, log or store untrusted data. Be careful when throwing an exception.

  • Qrexec sanitizes the argument, but if you will use the argument to something that you know has an even stricter syntax, it must be sanitized also.

  • The payload is received raw, in bytes. It must always be sanitized before being passed to functions that expect it to be already trusted.

  • When sanitizing data, if it may reveal information from the system, such as object existence, then, throw qubes.exc.PermissionDenied to avoid leaking object existence or delay reveal till after admin-permission.

.# Fire permission event

  • Must only pass sanitized information.

  • The most commonly used is qubes.api.AbstractQubesAPI.fire_event_for_permission(), which fires event admin-permission directly, used by qubes.api.admin.AdminExtension. For more complex cases, involving global information such as fetching objects from different destinations, qubes.api.AbstractQubesAPI.fire_event_for_filter() is more appropriate, as it fires admin-permission for each operation required.

.# Action

  • The client is fully authorized at this stage, it passed Qrexec policy evaluation and Qubesd admin-permission. The server may be aware that some resource could not be served before the admin-permisison, but it was not allowed to reveal at that stage, now it can reveal what and why it failed. A custom exception derived from qubes.exc.QubesException can be used to allow the client to handle it gracefully.

  • Act.

@qubes.api.method(
     "dest.feat.Set",      # RPC name
     wants_arg=True,       # Argument must be provided
     wants_payload=None,   # Payload can be provided
     dest_adminvm=False,   # Target must not be AdminVM
     scope="global",       # Applies to the whole system
     read=True,            # Will read system information
     write=True,           # Will write information to the system
 )
 async def dest_feat_set(self, untrusted_payload):
     """
     Set destination feature

     name:  self.arg
     value: untrusted_payload
     """
     # Qrexec sanitizes self.arg, but our feature name can only be made of
     # letters. The client should have know to not make such a request in the
     # first case, therefore it throws qubes.exc.ProtocolError.
     allowed_chars = string.ascii_letters
     self.enforce(
         all(c in allowed_chars for c in self.arg),
         reason="Feature name must be in safe set: " + allowed_chars,
     )

     # Payload is in bytes and we receive it without being sanitized
     # previously. We only want to allow values to be in ASCII.
     try:
         untrusted_value = untrusted_payload.decode("ascii", errors="strict")
     except UnicodeDecodeError:
         raise qubes.exc.ProtocolError("Value contains non-ASCII characters")
     # Delete untrusted payload prevent using it.
     del untrusted_payload

     # Second sanitization of the value
     if re.match(r"\A[\x20-\x7E]*\Z", untrusted_value) is None:
         raise qubes.exc.ProtocolError(
             f"{self.arg} value contains illegal characters"
         )
     # Delete untrusted value prevent using it.
     value = untrusted_value
     del untrusted_value

     # In this case, we just want to allow setting value to features that are
     # already set. We want to hide hide "absent" feature from "prohibited"
     # feature (see "admin-permission" below), therefore, it throws
     # qubes.exc.PermissionDenied.
     self.enforce_arg(
         wants=self.dest.features.keys(),
         short_reason="destination features",
     )

     # Event "admin-permission" is used to prohibit certain API calls from
     # qubesd when qrexec cannot possibly be that restrictive, as it doesn't
     # have full knowledge of the system nor the policy is expressive enough.
     # Only trusted data should be passed to this method.
     # Throws qubes.exc.PermissionDenied if call is prohibited.
     self.fire_event_for_permission(value=value)

     # The server is in a bad mood today. Let the user know we will not
     # serve them today.
     if True:
         raise qubes.exc.QubesException(
             "Not in a good mood today, feature '%r' doesn't look nice" %
             self.arg
         )

     # All validation has passed, we can return the requested data.
     self.dest.features[self.arg] = value
     self.app.save()

Inheritance diagram

digraph inheritance0924d1d3e9 { bgcolor=transparent; rankdir=LR; size="8.0, 12.0"; "asyncio.protocols.BaseProtocol" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Common base class for protocol interfaces."]; "asyncio.protocols.Protocol" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Interface for stream protocol."]; "asyncio.protocols.BaseProtocol" -> "asyncio.protocols.Protocol" [arrowsize=0.5,style="setlinewidth(0.5)"]; "qubes.api.AbstractQubesAPI" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Common code for Qubes Management Protocol handling"]; "qubes.api.QubesDaemonProtocol" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled"]; "asyncio.protocols.Protocol" -> "qubes.api.QubesDaemonProtocol" [arrowsize=0.5,style="setlinewidth(0.5)"]; }

Module contents

qubes.api.admin

alias of <module ‘qubes.api.admin’ from ‘/home/docs/checkouts/readthedocs.org/user_builds/qubes-core-admin/checkouts/latest/qubes/api/admin.py’>

qubes.api.internal

alias of <module ‘qubes.api.internal’ from ‘/home/docs/checkouts/readthedocs.org/user_builds/qubes-core-admin/checkouts/latest/qubes/api/internal.py’>

qubes.api.misc

alias of <module ‘qubes.api.misc’ from ‘/home/docs/checkouts/readthedocs.org/user_builds/qubes-core-admin/checkouts/latest/qubes/api/misc.py’>