policy.RegisterArgument 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. #!/usr/bin/env python3
  2. #
  3. # The Qubes OS Project, https://www.qubes-os.org/
  4. #
  5. # Copyright (C) 2017 Wojtek Porczyk <woju@invisiblethingslab.com>
  6. #
  7. # This library is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU Lesser General Public
  9. # License as published by the Free Software Foundation; either
  10. # version 2.1 of the License, or (at your option) any later version.
  11. #
  12. # This library is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. # Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public
  18. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  19. #
  20. '''policy.RegisterArgument
  21. This qrexec is meant for services, which require some kind of "registering"
  22. before use (say ``example.Register`` and ``example.Perform+ARGUMENT``). After
  23. registering, the backend should invoke this call with frontend as the intended
  24. destination, with the actual service in argument of this call and the argument
  25. as the payload. The policy generated will be a single line with explicit
  26. frontend and backend domain names, and a plain "allow", without further
  27. qualifiers.
  28. The call allows for registering an argument only once, for one frontend domain.
  29. There is not possibility of deregistering or reregistering for another frontend.
  30. The backend can always register another argument for any frontend, including
  31. one that is already registered for some other argument.
  32. By default this qrexec is disabled by policy. To actually use it you should
  33. drop a policy for an exact call you want to register which will redirect the
  34. call to dom0.
  35. .. code-block:: none
  36. :caption: /etc/qubes-rpc/policy/policy.RegisterArgument+example.Perform
  37. backendvm $anyvm allow,target=dom0
  38. It will generate, for argument ``EXAMPLE``:
  39. .. code-bloc:: none
  40. :caption: /etc/qubes-rpc/policy/example.Perform+EXAMPLE
  41. frontendvm backendvm allow
  42. '''
  43. import logging
  44. import os
  45. import string
  46. import sys
  47. import pathlib
  48. POLICY_PATH = pathlib.Path('/etc/qubes-rpc/policy')
  49. POLICY_RULE = '{frontend} {backend} allow\n'
  50. # linux-utils/qrexec-lib/qrexec.h
  51. MAX_ARGUMENT_LEN = 64
  52. # core-admin-linux/qrexec/qrexec-daemon.c
  53. VALID_CHARS = set(map(ord, string.ascii_letters + string.digits + '-._'))
  54. def die(*args, **kwargs):
  55. logging.error(*args, **kwargs)
  56. sys.exit(1)
  57. def main():
  58. # pylint: disable=missing-docstring
  59. logging.basicConfig(
  60. level=logging.WARNING,
  61. filename='/var/log/qubes/policy-register.log',
  62. format='%(asctime)s %(message)s')
  63. backend = os.environ['QREXEC_REMOTE_DOMAIN']
  64. frontend = os.environ['QREXEC_REQUESTED_TARGET']
  65. rpcname = os.environ['QREXEC_SERVICE_ARGUMENT']
  66. logging.debug('%s %s → %s request, reading argument',
  67. rpcname, frontend, backend)
  68. untrusted_argument = sys.stdin.buffer.read(MAX_ARGUMENT_LEN)
  69. untrusted_overflow = sys.stdin.buffer.read(1)
  70. sys.stdin.buffer.close()
  71. if untrusted_overflow:
  72. die('%s: %s → %s request refused: argument too long',
  73. rpcname, frontend, backend)
  74. if not untrusted_argument:
  75. die('%s: %s → %s request refused: empty argument',
  76. rpcname, frontend, backend)
  77. if any(c not in VALID_CHARS for c in untrusted_argument):
  78. die('%s: %s → %s request refused: invalid argument',
  79. rpcname, frontend, backend)
  80. # argument may also be too long, so that length of rpcname, separator and
  81. # argument exceed 64 bytes, but that's fine, the call just wont work
  82. argument = untrusted_argument
  83. del untrusted_argument
  84. argument = argument.decode('ascii', errors='strict')
  85. filename = '{}+{}'.format(rpcname, argument)
  86. logging.debug('%s %s → %s argument %s filename %s',
  87. rpcname, frontend, backend, argument, filename)
  88. try:
  89. # the 'x' enforces that argument cannot be registered twice
  90. with open(str(POLICY_PATH / filename), 'x') as file:
  91. rule = POLICY_RULE.format(frontend=frontend, backend=backend)
  92. logging.warning('%s: %s → %s %s argument allowed',
  93. rpcname, frontend, backend, argument)
  94. logging.debug('%s: %s → %s %s adding rule %r',
  95. rpcname, frontend, backend, rule)
  96. file.write(rule)
  97. except FileExistsError:
  98. die('%s: %s → %s %s argument failed: file exists')
  99. if __name__ == '__main__':
  100. main()