perf: update rdp sign

This commit is contained in:
ibuler
2026-07-08 15:34:24 +08:00
parent 7ac82e1ca5
commit 4d48971d19
2 changed files with 154 additions and 162 deletions

View File

@@ -2,9 +2,11 @@ import base64
import json
import os
import urllib.parse
import subprocess
from struct import pack
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import pkcs7
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
@@ -50,6 +52,154 @@ class RDPFileClientProtocolURLMixin:
request: Request
get_serializer: callable
RDP_SIGN_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'),
('redirectwebauthn:i:', 'RedirectWebAuthn'),
]
@classmethod
def _collect_rdp_sign_lines(cls, settings_lines):
signnames = []
signlines = []
for prefix, sign_name in cls.RDP_SIGN_SECURE_SETTINGS:
for line in settings_lines:
if line.startswith(prefix):
signnames.append(sign_name)
signlines.append(line)
return signnames, signlines
@classmethod
def _try_sign_rdp_content(cls, content):
if not settings.RDP_SIGN_ENABLED:
return content
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 content
certfile = os.path.join(cert_dir, settings.RDP_SIGN_CERT)
if not os.path.exists(certfile):
logger.error(f'rdp sign cert file [{certfile}] not exists')
return content
keyfile = os.path.join(cert_dir, settings.RDP_SIGN_CERT_KEY)
if not os.path.exists(keyfile):
logger.warning(f'rdp sign cert file [{keyfile}] not exists')
keyfile = None
return content
settings_lines = []
full_address = None
alternate_full_address = None
for line in content.splitlines():
line = line.strip()
if not line:
continue
if line.startswith('signature:s:') or line.startswith('signscope:s:'):
continue
if line.startswith('full address:s:'):
full_address = line[15:]
elif line.startswith('alternate full address:s:'):
alternate_full_address = line[25:]
settings_lines.append(line)
# Keep alternate full address aligned with full address to prevent tampering.
if full_address and not alternate_full_address:
settings_lines.append(f'alternate full address:s:{full_address}')
signnames, signlines = cls._collect_rdp_sign_lines(settings_lines)
if not signnames or not signlines:
return content
msgtext = '\r\n'.join(signlines) + '\r\n' + 'signscope:s:' + ','.join(signnames) + '\r\n' + '\x00'
msgblob = msgtext.encode('UTF-16LE')
try:
with open(certfile, 'rb') as f:
cert_pem = f.read()
cert = x509.load_pem_x509_certificate(cert_pem)
if keyfile:
with open(keyfile, 'rb') as f:
key_pem = f.read()
else:
key_pem = cert_pem
private_key = serialization.load_pem_private_key(key_pem, password=None)
pkcs7_der = (
pkcs7.PKCS7SignatureBuilder()
.set_data(msgblob)
.add_signer(cert, private_key, hashes.SHA256())
.sign(
serialization.Encoding.DER,
[
pkcs7.PKCS7Options.Binary,
pkcs7.PKCS7Options.DetachedSignature,
pkcs7.PKCS7Options.NoAttributes,
],
)
)
except OSError as e:
logger.warning('RDP file sign failed to read cert/key: %s', e)
return content
except ValueError as e:
logger.warning('RDP file sign failed (invalid cert/key PEM): %s', e)
return content
except Exception as e:
logger.warning('RDP file sign failed: %s', e)
return content
msgsig = pack('<I', 0x00010001)
msgsig += pack('<I', 0x00000001)
msgsig += pack('<I', len(pkcs7_der))
msgsig += pkcs7_der
sigval = base64.b64encode(msgsig).decode('ascii')
signed_lines = settings_lines + [f'signscope:s:{",".join(signnames)}', f'signature:s:{sigval}']
return '\n'.join(signed_lines) + '\n'
def get_rdp_file_info(self, token: ConnectionToken):
rdp_options = {
'full address:s': '',
@@ -165,166 +315,8 @@ 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
content = self._try_sign_rdp_content(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("<I", 0x00010001) # unknown DWORD value
msgsig += pack("<I", 0x00000001) # unknown DWORD value
msgsig += pack("<I", len(opensslout))
msgsig += opensslout
sigval = base64.b64encode(msgsig).decode("ascii")
parts = []
parts.append("\r\n".join(rdp_settings))
parts.append("signscope:s:" + ",".join(signnames))
parts.append("signature:s:" + sigval)
signed_content = "\r\n".join(parts) + "\r\n"
return signed_content
@staticmethod
def escape_name(name):

View File

@@ -797,8 +797,8 @@ class Config(dict):
# rdp sign cert
'RDP_SIGN_ENABLED': False,
'RDP_SIGN_CERT': 'signer.crt',
'RDP_SIGN_CERT_KEY': 'signer.key',
'RDP_SIGN_CERT': 'rdp_signer.crt',
'RDP_SIGN_CERT_KEY': 'rdp_signer.key',
}
old_config_map = {