diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 34ae32e0e..5bc1ace13 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -2,6 +2,8 @@ import base64 import json import os import urllib.parse +import subprocess +from struct import pack from django.conf import settings from django.http import HttpResponse @@ -162,7 +164,166 @@ class RDPFileClientProtocolURLMixin: for k, v in rdp_options.items(): content += f'{k}:{v}\n' + if settings.RDP_SIGN_ENABLED: + signed_content = self.signed_rdp_content(content) + if signed_content: + content = signed_content + return filename, content + + @staticmethod + def signed_rdp_content(rdp_file_content: str): + cert_dir = os.path.join(settings.PROJECT_DIR, 'data', 'certs') + if not os.path.exists(cert_dir): + logger.error(f'rdp sign cert dir [{cert_dir}] not exists') + return None + + crt_path = os.path.join(cert_dir, settings.RDP_SIGN_CERT) + if not os.path.exists(crt_path): + logger.error(f'rdp sign cert file [{crt_path}] not exists') + return None + + key_path = os.path.join(cert_dir, settings.RDP_SIGN_CERT_KEY) + if not os.path.exists(key_path): + logger.warning(f'rdp sign cert file [{key_path}] not exists') + key_path = None + + securesettings = [ + ["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"], + ] + + rdp_settings = list() + signlines = list() + signnames = list() + + lines = [v.strip() for v in rdp_file_content.splitlines()] + + fulladdress = None + alternatefulladdress = None + + for v in lines: + if v.startswith("full address:s:"): + fulladdress = v[15:] + elif v.startswith("alternate full address:s:"): + alternatefulladdress = v[25:] + elif v.startswith("signature:s:"): + continue + elif v.startswith("signscope:s:"): + continue + rdp_settings.append(v) + + # prevent hacks via alternate full address + if fulladdress and not alternatefulladdress: + rdp_settings.append("alternate full address:s:" + fulladdress) + + for s in securesettings: + for v in rdp_settings: + if v.startswith(s[0]): + signnames.append(s[1]) + signlines.append(v) + + msgtext = ( + "\r\n".join(signlines) + + "\r\n" + + "signscope:s:" + + ",".join(signnames) + + "\r\n" + + "\x00" + ) + + msgblob = msgtext.encode("UTF-16LE") + + params = ["openssl", "smime", "-sign", "-binary"] + params += ["-signer", crt_path] + params += ["-outform", "DER"] + params += ["-noattr", "-nosmimecap"] + + if key_path is not None: + params += ["-inkey", key_path] + + try: + proc = subprocess.Popen( + params, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + opensslout, opensslerr = proc.communicate(msgblob) + except OSError as e: + logger.error("Error calling openssl command: %s", e.strerror) + return None + + retcode = proc.poll() + + if retcode != 0: + emsg = "openssl command failed (return code #{0:d})".format(retcode) + if opensslerr is not None: + emsg += ":\n" + emsg += opensslerr.decode("utf-8", errors="replace") + logger.error(emsg) + return None + + # The Microsoft rdpsign.exe adds a 12 byte header to the signature + # before it gets base64 encoded + # The meaning of the first 8 bytes is still unknown + msgsig = pack("