mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-07-11 09:25:03 +00:00
perf: update rdp sign
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user