Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client
13 changes: 13 additions & 0 deletions server/src/uds/REST/methods/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

from uds import models
from uds.core import consts, exceptions, types
from uds.core.managers import crypto
from uds.core.managers.crypto import CryptoManager
from uds.core.managers.userservice import UserServiceManager
from uds.core.exceptions.services import ServiceNotReadyError
Expand Down Expand Up @@ -107,6 +108,16 @@ def test(self) -> dict[str, typing.Any]:
"""
return Client.result(_('Correct'))

def sign_rdp(self, rdp: str) -> dict[str, typing.Any]:
try:
logger.debug('Signing RDP (input):\n%s', rdp)
signed = crypto.CryptoManager.manager().sign_rdp(rdp)
logger.debug('Signed RDP (output):\n%s', signed)
return Client.result(signed)
except Exception as e:
logger.exception('Error signing RDP')
return Client.result(error=str(e))

def process(self, ticket: str, scrambler: str) -> dict[str, typing.Any]:
info: typing.Optional[types.services.UserServiceInfo] = None
hostname = self._params.get('hostname', '') # Or if hostname is not included...
Expand Down Expand Up @@ -251,6 +262,8 @@ def post(self) -> dict[str, typing.Any]:
except Exception:
# If something goes wrong, log it as debug
pass
case 'rdp_signature':
return self.sign_rdp(self._params.get('rdp') or '')
case _:
return Client.result(error='Invalid command')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from django.conf import settings

from uds.core.util import singleton
from . import rdp

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -340,3 +341,10 @@ def sha(self, value: typing.Union[str, bytes]) -> str:
value = value.encode()

return hashlib.sha3_256(value).hexdigest()

# RDP related
def sign_rdp(self, data: str) -> str:
"""
Signs the data using the key and returns the signature.
"""
return rdp.sign_rdp(data)
183 changes: 183 additions & 0 deletions server/src/uds/core/managers/crypto/rdp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2023 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
# Read key & server from /etc/certs{key,server}.pem (read from config file))
from django.conf import settings

# --- RDP Secure Settings (order matters for mstsc.exe) ---
_RDP_SECURE_SETTINGS = [
('full address:s:', 'Full Address'),
('alternate full address:s:', 'Alternate Full Address'),
('pcb:s:', 'PCB'),
('use redirection server name:i:', 'Use Redirection Server Name'),
('server port:i:', 'Server Port'),
('negotiate security layer:i:', 'Negotiate Security Layer'),
('enablecredsspsupport:i:', 'EnableCredSspSupport'),
('disableconnectionsharing:i:', 'DisableConnectionSharing'),
('autoreconnection enabled:i:', 'AutoReconnection Enabled'),
('gatewayhostname:s:', 'GatewayHostname'),
('gatewayusagemethod:i:', 'GatewayUsageMethod'),
('gatewayprofileusagemethod:i:', 'GatewayProfileUsageMethod'),
('gatewaycredentialssource:i:', 'GatewayCredentialsSource'),
('support url:s:', 'Support URL'),
('promptcredentialonce:i:', 'PromptCredentialOnce'),
('require pre-authentication:i:', 'Require pre-authentication'),
('pre-authentication server address:s:', 'Pre-authentication server address'),
('alternate shell:s:', 'Alternate Shell'),
('shell working directory:s:', 'Shell Working Directory'),
('remoteapplicationprogram:s:', 'RemoteApplicationProgram'),
('remoteapplicationexpandworkingdir:s:', 'RemoteApplicationExpandWorkingdir'),
('remoteapplicationmode:i:', 'RemoteApplicationMode'),
('remoteapplicationguid:s:', 'RemoteApplicationGuid'),
('remoteapplicationname:s:', 'RemoteApplicationName'),
('remoteapplicationicon:s:', 'RemoteApplicationIcon'),
('remoteapplicationfile:s:', 'RemoteApplicationFile'),
('remoteapplicationfileextensions:s:', 'RemoteApplicationFileExtensions'),
('remoteapplicationcmdline:s:', 'RemoteApplicationCmdLine'),
('remoteapplicationexpandcmdline:s:', 'RemoteApplicationExpandCmdLine'),
('prompt for credentials:i:', 'Prompt For Credentials'),
('authentication level:i:', 'Authentication Level'),
('audiomode:i:', 'AudioMode'),
('redirectdrives:i:', 'RedirectDrives'),
('redirectprinters:i:', 'RedirectPrinters'),
('redirectcomports:i:', 'RedirectCOMPorts'),
('redirectsmartcards:i:', 'RedirectSmartCards'),
('redirectposdevices:i:', 'RedirectPOSDevices'),
('redirectclipboard:i:', 'RedirectClipboard'),
('devicestoredirect:s:', 'DevicesToRedirect'),
('drivestoredirect:s:', 'DrivesToRedirect'),
('loadbalanceinfo:s:', 'LoadBalanceInfo'),
('redirectdirectx:i:', 'RedirectDirectX'),
('rdgiskdcproxy:i:', 'RDGIsKDCProxy'),
('kdcproxyname:s:', 'KDCProxyName'),
('eventloguploadaddress:s:', 'EventLogUploadAddress'),
]


import base64
import struct
import typing
import logging
from cryptography.hazmat.primitives.serialization import pkcs7, Encoding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
from cryptography import x509

logger = logging.getLogger(__name__)

def _load_cert_key_chain():
"""
Load certificate, key and chain from global configuration.
"""
server_pem_path = getattr(settings, 'RDP_SIGN_CERT', '/etc/certs/server.pem')
key_pem_path = getattr(settings, 'RDP_SIGN_KEY', '/etc/certs/key.pem')
with open(server_pem_path, 'r') as f:
pem_data = f.read()
# Split all certificate blocks
cert_blocks = pem_data.split('-----END CERTIFICATE-----')
certs = []
for block in cert_blocks:
block = block.strip()
if block:
block += '\n-----END CERTIFICATE-----\n'
certs.append(block)
if not certs:
raise ValueError("No certificates found in server.pem")
# First block is the leaf, the rest is the chain
cert = x509.load_pem_x509_certificate(certs[0].encode(), default_backend())
chain = [x509.load_pem_x509_certificate(c.encode(), default_backend()) for c in certs[1:]] if len(certs) > 1 else []
with open(key_pem_path, 'rb') as f:
key_pem = f.read()
key = serialization.load_pem_private_key(key_pem, password=None, backend=default_backend())
return cert, key, chain

def sign_rdp_settings(settings_lines: typing.List[str], cert=None, key=None, chain=None) -> typing.Tuple[str, typing.List[str]]:
"""
Sign the RDP configuration lines and return (base64_signature, signnames).
"""
# Filter and order the lines to sign
signlines = []
signnames = []
for k, name in _RDP_SECURE_SETTINGS:
for line in settings_lines:
if line.startswith(k):
signnames.append(name)
signlines.append(line)

msgtext = '\r\n'.join(signlines) + '\r\nsignscope:s:' + ','.join(signnames) + '\r\n' + '\x00'
msgblob = msgtext.encode('utf-16le')

if cert is None or key is None:
cert, key, chain = _load_cert_key_chain()

# Use PKCS7 to sign, including the chain if present
builder = pkcs7.PKCS7SignatureBuilder().set_data(msgblob)
builder = builder.add_signer(cert, key, hashes.SHA256())
if chain:
for c in chain:
builder = builder.add_certificate(c)
signature = builder.sign(Encoding.DER, [pkcs7.PKCS7Options.DetachedSignature, pkcs7.PKCS7Options.Binary])

# Add 12-byte header as rdpsign.exe does
msgsig = struct.pack('<I', 0x00010001)
msgsig += struct.pack('<I', 0x00000001)
msgsig += struct.pack('<I', len(signature))
msgsig += signature

sigval = base64.b64encode(msgsig).decode('ascii')
return sigval, signnames

def sign_rdp(rdp_text: str, cert=None, key=None, chain=None) -> str:
"""
Sign a complete RDP file (text) and return the resulting .rdp with
the signscope:s: and signature:s: lines appended at the end.
"""
# Strip previous signature and signscope lines and empty lines
lines = [
l.strip()
for l in rdp_text.splitlines()
if l.strip() and not l.startswith('signature:s:') and not l.startswith('signscope:s:')
]
# If alternate full address is missing, add it
fulladdress = None
alternatefulladdress = None
for l in lines:
if l.startswith('full address:s:'):
fulladdress = l[15:]
elif l.startswith('alternate full address:s:'):
alternatefulladdress = l[25:]
if fulladdress and not alternatefulladdress:
lines.append('alternate full address:s:' + fulladdress)

sigval, signnames = sign_rdp_settings(lines, cert, key, chain)
lines.append('signscope:s:' + ','.join(signnames))
lines.append('signature:s:' + sigval)
return '\r\n'.join(lines) + '\r\n'
15 changes: 15 additions & 0 deletions server/src/uds/transports/RDP/rdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from django.utils.translation import gettext_noop as _

from uds.core import types
from uds.models.ticket_store import TicketStore

from .rdp_base import BaseRDPTransport
from .rdp_file import RDPFile
Expand Down Expand Up @@ -150,12 +151,26 @@ def get_transport_script( # pylint: disable=too-many-locals
r.enforced_shares = self.enforce_drives.value
r.redir_usb = self.allow_usb_redirection.value

# ticket_for_sign = TicketStore.create(None)

ticket_for_sign = TicketStore.create(
{
'user': userservice.user.uuid if userservice.user else None,
'userservice': userservice.uuid,
'type': 'rdp',
},
validity=30,
)

logger.debug('Created ticket for RDP signing: %s', ticket_for_sign)

sp: collections.abc.MutableMapping[str, typing.Any] = {
'password': ci.password,
'this_server': request.build_absolute_uri('/'),
'ip': ip,
'port': self.rdp_port.value, # As string, because we need to use it in the template
'address': r.address,
'ticket_sign': ticket_for_sign,
}

if os.os == types.os.KnownOS.WINDOWS:
Expand Down
10 changes: 10 additions & 0 deletions server/src/uds/transports/RDP/rdptunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,15 @@ def get_transport_script( # pylint: disable=too-many-locals
r.enforced_shares = self.enforce_drives.value
r.redir_usb = self.allow_usb_redirection.value

ticket_for_sign = TicketStore.create(
{
'user': userservice.user.uuid if userservice.user else None,
'userservice': userservice.uuid,
'type': 'rdp',
},
validity=30,
)

sp: collections.abc.MutableMapping[str, typing.Any] = {
'tunHost': tunnel_host,
'tunPort': tunnel_port,
Expand All @@ -192,6 +201,7 @@ def get_transport_script( # pylint: disable=too-many-locals
'password': ci.password,
'this_server': request.build_absolute_uri('/'),
'tunnel_key': key,
'ticket_sign': ticket_for_sign,
}

if os.os == types.os.KnownOS.WINDOWS:
Expand Down
3 changes: 3 additions & 0 deletions server/src/uds/transports/RDP/scripts/windows/direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@

# The password must be encoded, to be included in a .rdp file, as 'UTF-16LE' before protecting (CtrpyProtectData) it in order to work with mstsc
theFile = sp['as_file'].format(password=password) # type: ignore

theFile = tools.sign_rdp(theFile, api, sp['ticket_sign']) # type: ignore

filename = tools.saveTempFile(theFile)

executable = tools.findApp('mstsc.exe')
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b0NrFPNRgkQmGycaL/gUhFKShW34N3Yto33JyDT9ructKOTEzT8qCEnvp5ypb0vwZQBhCfya0ExGDO77DdRPb2QAvtQylPxaX+D7FdLAKZO8WOw+clCJGJHFlpiAi7a1lY0ve6dfJotu47vNppmOy5RGO1Iz9FMQuOpq0xNXcrGz9I5zez47Se0FkhU1XlYgrrI8uexQqc8faz+nw6fJE4ADnfqo+b6mJmRIm7gbE9VyMZz6NR65zqtcyOWgmRrDOO3w6dirEYOIES2GFfZXOl4L+5bIDTVbtrYGoTtPIgmom8fjFfOP2qWAhjQ7jsjDqC0pPshOlNqB4FyORoAEzQ10yt53bPJHaOe/9uzW75THNGCj8AVntzbLGDghdJG49Yv9gAxJPFpdkGhtesy92Q0pryDjtTtLBtTyWvj9iCpUremYp71tROFHdEY40ypG7YDmDHNdkK6vz99MsFwHpcjs9XnHAJlaJHy96FdI6dHBC4ePlaJSVABOb9SS74WyYVB/VOF6bZ55mbvD7XpzzsG7fk/JV6If047tULGnCdWJCvOZ05rI0H1nUJAwgg42VmOKxNKJnBKdP0hVPuvRg2L2pNDioocXxnXvYfUWBr6bq/6Vkv/qrkkkWy+XMhTSGD9nwskhpFdOMNfjeelr50bSGcl2QGzEO2SnKzrfdTo=
imyDe6MtGTaU9Pc8piU/4+xXa1hO9t5anGyUnuIqKSQenEc2ZxYZEGY6G6UDf/hLINsTt67T31avJouB4jAZmo2jY3squ+b1CD2aecZHyzHb270Yv+ieNQnSrPic7niU/EWhaG+bZw+AdB8LkbVaaZisIyn9SEWoYD8I1lZTUyHtL7lIRdXBtP/XcVxLx1/nXSbunfYMShFZu4pqk6cX5jnopFcnPF/S4ETq0Xv2ehXP6eeYeE5dkSOPoM51swXbkdK9mPfeoM136BfB+Nz6gma1jmsxT6hCcn0eYQ/hev+/JaP29HI3SN760cP8vc+asSDIwJmMXB7jhrLjK8qF0LtLPw7iSQaKLbd9yZhLT6DJHbf63dE9lCI6UCPhQIRG+dSv7rSKw3bvWZakZEKcenn1AvRVi/ZEfaS1Gy6sgNund+u0P+NSzhh4fZ41PMfqbY71O0QfozzS/W2dyv4EowBawcrAXMp+N/k8zeUhimy+v8LAC2xQw5tkqZFGmZ/2aNukoQoh3Dpsl1muQF0rY38qzYy4dcQsqyIM8QCcBNY8q+c/C2Wgio64vNMW36jKPJ0bCtnsgToXrzGB3hzEB/IA+RrWd5pXPspIrmkZQy4g3supiMkt/9xdMRdajZKGUTQcNQjvXbMPhqbt1nx9iWF+Bba7+o7Lq1aCgozyw54=
2 changes: 2 additions & 0 deletions server/src/uds/transports/RDP/scripts/windows/tunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
password=password, address='127.0.0.1:{}'.format(fs.server_address[1])
)

theFile = tools.sign_rdp(theFile, api, sp['ticket_sign']) # type: ignore

filename = tools.saveTempFile(theFile)
executable = tools.findApp('mstsc.exe')
if executable is None:
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
kG2nEc2XkOI45ag2onfRbTovNND1L5CMWi7XI6+S2Rl35A64NBhDJL5sr3yKh6nLMQ+xPE7aYARPJ15tzboR9LG5fUNcubwHqtexT/NBc6eV2tEbGskSCutpwh7lgyqb2nH4B51u0JIoJbKDuF09L+wk/yWJrTvNBfjW7RZ19f9zxBYO0MCoDl3pnBgX8IRn0wyu84PhY+78NOodOtAx3DuYlsa4i5aQ4Wq2bFsMGjS+gfd43ybn0gQIPhC9U/6QQ3Jeh49Ylw4p8iqdr/FDLsbSyvDh3cTKPP/kkaQfL/muqCXm7X3eUx434aoHM15NY3F7kNsUu34tX9ZL5wwivlf3kq09ygrkZUddPEt9/8YThHbPD5OpBeuYD30ofoYKQidlVj/w8uonPPo9cDpOxf26k673J53I24J2sD2yQ/7ouH4aXAsNTIVCd5iCSZuyOGf+6UArMW20SPNORaNwh0qdts8LumQo0latWgbphVKbtM/2XQA/v+g43olplzGJjlogVYC6L3E78L+1OjXcqXdGCcC/9y07M65sbDIH6X6ertP73daaZa8kHiFwJV+KFpaKLC1KeicgVV1rCb3VxcYkIlLIw0lXH7XQb2vMG9Aqag5JaYOyE3EKay8Ec/uxxow4w32EMGBVWd6VPUb/3NB3JcmjwY/DwgJUcdzJ6O4=
nUA3jqSOt+fqacp11hAcsBOJp7ffb4Hdj9IBUos7fX8VI+nnTpV75cL1eAJYH6sU3cq7t+IETId2Wld5pvh7l2CnY3c0Jg+R44rZaGxVKne7iSKz92RAqEz9fkpxr9nZ9mnAf0feN3NF+eF+fEXE90FsmGHR0K4os/8JHgfYeDpQNK6A4F8vq20w+BM4Y+yuokTkv0krkY5X64ofgX/Qo5mOApcSyiyVIuPfX9e5H53mMPPHJiBI1LnwYq8B9YVH2f6HEx+tVZW0a539v9e6vq4muSSjNzYu/6cHEMLEdicBjYTD7R3X2uPOlMlZnoUeZUsaZReTWe1XuVzLIe739+4q36y1HT9VUMznzAxq+x66u6LdQPJfShVZ9dX27WEDZwN1wY41nh/VcG6mlnHqWYRFSk1lRRx7eibZJfz9spJG6RhHQDNj3pMD8YF+SYeDQgYmREggVicSSG2sDfnfNNO+3xwSC7SaObYLajiNazNZOJUvvjuRI3KTOedluOxFbvQi1Q4CHCmHRqpE29ysMg3+dkB0GN6sSNB6fbvPif6ge22r/jFCiXO8WueYcZwI4S8aSXlxP8lG2KNnnN8JN8fs03yFvMeZHtRQDoFoDErcBeNGVZlKnHPIaubH5Lj9phDL3YjsSVLR6rFvc6pApbnMI4FyySQRF54HTgXBaFQ=
Loading