From 4d48971d19196d3d50d1b966020ea4e754744ed9 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 8 Jul 2026 15:34:24 +0800 Subject: [PATCH] perf: update rdp sign --- apps/authentication/api/connection_token.py | 312 ++++++++++---------- apps/jumpserver/conf.py | 4 +- 2 files changed, 154 insertions(+), 162 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index b50325a98..970808680 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -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('