diff --git a/MeshCentralRouter.csproj b/MeshCentralRouter.csproj index e4d7348..37b4228 100644 --- a/MeshCentralRouter.csproj +++ b/MeshCentralRouter.csproj @@ -295,6 +295,7 @@ + AddPortMapForm.cs diff --git a/Program.cs b/Program.cs index 4f59844..a87d4ba 100644 --- a/Program.cs +++ b/Program.cs @@ -15,20 +15,40 @@ limitations under the License. */ using System; +using System.Collections.Specialized; using System.IO; using System.Text; +using System.Web; using System.Windows.Forms; namespace MeshCentralRouter { static class Program { + public static string LockToHostname = null; + public static string LockToServerId = null; + /// /// The main entry point for the application. /// [STAThread] static void Main(string[] args) { + // If this application is signed, get the URL of the signature, this will be used to lock this application to a server. + Uri signedUrl = WinCrypt.GetSignatureUrl(System.Reflection.Assembly.GetEntryAssembly().Location); + if (signedUrl != null) + { + NameValueCollection urlArguments = HttpUtility.ParseQueryString(signedUrl.Query); + if (urlArguments["serverid"] != null) + { + LockToServerId = urlArguments["serverid"]; + LockToHostname = signedUrl.Host; + } + } + + LockToHostname = "central.mesh.meshcentral.com"; + LockToServerId = "D99362D5ED8BAEA8BF9E743B34B242256370C460FD66CB62373C6CFCB204D6D707403E396CF0EF6DC2B3A42F735135FD"; + Uri authLoginUrl = null; // Setup settings & visual style diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs index 42ad1d4..7f3d1ab 100644 --- a/Properties/Resources.Designer.cs +++ b/Properties/Resources.Designer.cs @@ -536,6 +536,24 @@ namespace MeshCentralRouter.Properties { } } + /// + /// Looks up a localized string similar to Locked to host: {0}.. + /// + internal static string LockedToHost { + get { + return ResourceManager.GetString("LockedToHost", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Locked to server id: {0}.. + /// + internal static string LockedToServerId { + get { + return ResourceManager.GetString("LockedToServerId", resourceCulture); + } + } + /// /// Looks up a localized string similar to , {0} connections.. /// @@ -836,6 +854,15 @@ namespace MeshCentralRouter.Properties { } } + /// + /// Looks up a localized string similar to This executable is locked to only connect to {0}.. + /// + internal static string SignedExecutableServerLockError { + get { + return ResourceManager.GetString("SignedExecutableServerLockError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Slow. /// diff --git a/Properties/Resources.resx b/Properties/Resources.resx index dd2d065..2563f04 100644 --- a/Properties/Resources.resx +++ b/Properties/Resources.resx @@ -457,4 +457,13 @@ ..\Resources\icon-monitor1.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + Locked to host: {0}. + + + Locked to server id: {0}. + + + This executable is locked to only connect to {0}. + \ No newline at end of file diff --git a/readme.md b/readme.md index 8a7aa02..1d4ffb7 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,7 @@ MeshCentral Router is a Windows TCP/UDP port mapping tool. You first needs to ha A typical use of this tool is to perform a Remote Desktop RDP session with another computer over the Internet without the need to set a VPN. ## Video Tutorials + You can watch many tutorial videos on the [MeshCentral YouTube Channel](https://www.youtube.com/channel/UCJWz607A8EVlkilzcrb-GKg/videos). MeshCentral Router has it's own introduction video. Introduction to MeshCentral Router. diff --git a/src/MainForm.cs b/src/MainForm.cs index 23b2902..cbe586c 100644 --- a/src/MainForm.cs +++ b/src/MainForm.cs @@ -269,6 +269,15 @@ namespace MeshCentralRouter // Set automatic port map values if (authLoginUrl != null) { + // Check if we are locked to a server + if ((Program.LockToHostname != null) && (Program.LockToHostname != authLoginUrl.Host)) + { + forceExit = true; + MessageBox.Show(string.Format(Properties.Resources.SignedExecutableServerLockError, Program.LockToHostname), Properties.Resources.MeshCentralRouter, MessageBoxButtons.OK, MessageBoxIcon.Error); + Application.Exit(); + return; + } + string autoName = null; string autoNodeId = null; string autoRemoteIp = null; @@ -507,6 +516,30 @@ namespace MeshCentralRouter private void nextButton1_Click(object sender, EventArgs e) { + // Check if we are locked to a server + if (Program.LockToHostname != null) + { + bool ok = true; + if (authLoginUrl != null) + { + ok = (Program.LockToHostname == authLoginUrl.Host); + } + else + { + string host = serverNameComboBox.Text; + int i = host.IndexOf("?key="); + if (i >= 0) { host = host.Substring(0, i); } + i = host.IndexOf(":"); + if (i >= 0) { host = host.Substring(0, i); } + ok = (Program.LockToHostname == host); + } + if (ok == false) + { + MessageBox.Show(string.Format(Properties.Resources.SignedExecutableServerLockError, Program.LockToHostname), Properties.Resources.MeshCentralRouter, MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + } + // Attempt to login addButton.Enabled = false; addRelayButton.Enabled = false; @@ -1064,6 +1097,13 @@ namespace MeshCentralRouter Uri authLoginUrl2 = new Uri(args); + // Check if we are locked to a server + if ((Program.LockToHostname != null) && (Program.LockToHostname != authLoginUrl2.Host)) + { + MessageBox.Show(string.Format(Properties.Resources.SignedExecutableServerLockError, Program.LockToHostname), Properties.Resources.MeshCentralRouter, MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + // Set automatic port map values if (authLoginUrl2 != null) { diff --git a/src/WinCrypt.cs b/src/WinCrypt.cs new file mode 100644 index 0000000..98c669d --- /dev/null +++ b/src/WinCrypt.cs @@ -0,0 +1,382 @@ +using System; +using System.Runtime.InteropServices; +using System.Security.Cryptography.Pkcs; +using System.Security.Cryptography; + +namespace MeshCentralRouter +{ + static class WinCrypt + { + [StructLayout(LayoutKind.Sequential)] + public struct BLOB + { + public int cbData; + public IntPtr pbData; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CRYPT_ALGORITHM_IDENTIFIER + { + public String pszObjId; + BLOB Parameters; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CERT_ID + { + public int dwIdChoice; + public BLOB IssuerSerialNumberOrKeyIdOrHashId; + } + + [StructLayoutAttribute(LayoutKind.Sequential)] + public struct SIGNER_SUBJECT_INFO + { + /// DWORD->unsigned int + public uint cbSize; + + /// DWORD* + public System.IntPtr pdwIndex; + + /// DWORD->unsigned int + public uint dwSubjectChoice; + + /// SubjectChoiceUnion + public SubjectChoiceUnion Union1; + } + + [StructLayoutAttribute(LayoutKind.Explicit)] + public struct SubjectChoiceUnion + { + + /// SIGNER_FILE_INFO* + [FieldOffsetAttribute(0)] + public System.IntPtr pSignerFileInfo; + + /// SIGNER_BLOB_INFO* + [FieldOffsetAttribute(0)] + public System.IntPtr pSignerBlobInfo; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CERT_NAME_BLOB + { + public uint cbData; + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] + public byte[] pbData; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CRYPT_INTEGER_BLOB + { + public UInt32 cbData; + public IntPtr pbData; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CRYPT_ATTR_BLOB + { + public uint cbData; + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] + public byte[] pbData; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CRYPT_ATTRIBUTE + { + [MarshalAs(UnmanagedType.LPStr)] + public string pszObjId; + public uint cValue; + [MarshalAs(UnmanagedType.LPStruct)] + public CRYPT_ATTR_BLOB rgValue; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CMSG_SIGNER_INFO + { + public int dwVersion; + private CERT_NAME_BLOB Issuer; + CRYPT_INTEGER_BLOB SerialNumber; + CRYPT_ALGORITHM_IDENTIFIER HashAlgorithm; + CRYPT_ALGORITHM_IDENTIFIER HashEncryptionAlgorithm; + BLOB EncryptedHash; + CRYPT_ATTRIBUTE[] AuthAttrs; + CRYPT_ATTRIBUTE[] UnauthAttrs; + } + + [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern Boolean CryptQueryObject( + int dwObjectType, + IntPtr pvObject, + int dwExpectedContentTypeFlags, + int dwExpectedFormatTypeFlags, + int dwFlags, + out int pdwMsgAndCertEncodingType, + out int pdwContentType, + out int pdwFormatType, + ref IntPtr phCertStore, + ref IntPtr phMsg, + ref IntPtr ppvContext); + + + [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern Boolean CryptMsgGetParam( + IntPtr hCryptMsg, + int dwParamType, + int dwIndex, + IntPtr pvData, + ref int pcbData + ); + + [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern Boolean CryptMsgGetParam( + IntPtr hCryptMsg, + int dwParamType, + int dwIndex, + [In, Out] byte[] vData, + ref int pcbData + ); + + [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptDecodeObject( + uint CertEncodingType, + UIntPtr lpszStructType, + byte[] pbEncoded, + uint cbEncoded, + uint flags, + [In, Out] byte[] pvStructInfo, + ref uint cbStructInfo); + + + public const int CRYPT_ASN_ENCODING = 0x00000001; + public const int CRYPT_NDR_ENCODING = 0x00000002; + public const int X509_ASN_ENCODING = 0x00000001; + public const int X509_NDR_ENCODING = 0x00000002; + public const int PKCS_7_ASN_ENCODING = 0x00010000; + public const int PKCS_7_NDR_ENCODING = 0x00020000; + + public static UIntPtr PKCS7_SIGNER_INFO = new UIntPtr(500); + public static UIntPtr CMS_SIGNER_INFO = new UIntPtr(501); + + public static string szOID_RSA_signingTime = "1.2.840.113549.1.9.5"; + public static string szOID_RSA_counterSign = "1.2.840.113549.1.9.6"; + + //+------------------------------------------------------------------------- + // Get parameter types and their corresponding data structure definitions. + //-------------------------------------------------------------------------- + public const int CMSG_TYPE_PARAM = 1; + public const int CMSG_CONTENT_PARAM = 2; + public const int CMSG_BARE_CONTENT_PARAM = 3; + public const int CMSG_INNER_CONTENT_TYPE_PARAM = 4; + public const int CMSG_SIGNER_COUNT_PARAM = 5; + public const int CMSG_SIGNER_INFO_PARAM = 6; + public const int CMSG_SIGNER_CERT_INFO_PARAM = 7; + public const int CMSG_SIGNER_HASH_ALGORITHM_PARAM = 8; + public const int CMSG_SIGNER_AUTH_ATTR_PARAM = 9; + public const int CMSG_SIGNER_UNAUTH_ATTR_PARAM = 10; + public const int CMSG_CERT_COUNT_PARAM = 11; + public const int CMSG_CERT_PARAM = 12; + public const int CMSG_CRL_COUNT_PARAM = 13; + public const int CMSG_CRL_PARAM = 14; + public const int CMSG_ENVELOPE_ALGORITHM_PARAM = 15; + public const int CMSG_RECIPIENT_COUNT_PARAM = 17; + public const int CMSG_RECIPIENT_INDEX_PARAM = 18; + public const int CMSG_RECIPIENT_INFO_PARAM = 19; + public const int CMSG_HASH_ALGORITHM_PARAM = 20; + public const int CMSG_HASH_DATA_PARAM = 21; + public const int CMSG_COMPUTED_HASH_PARAM = 22; + public const int CMSG_ENCRYPT_PARAM = 26; + public const int CMSG_ENCRYPTED_DIGEST = 27; + public const int CMSG_ENCODED_SIGNER = 28; + public const int CMSG_ENCODED_MESSAGE = 29; + public const int CMSG_VERSION_PARAM = 30; + public const int CMSG_ATTR_CERT_COUNT_PARAM = 31; + public const int CMSG_ATTR_CERT_PARAM = 32; + public const int CMSG_CMS_RECIPIENT_COUNT_PARAM = 33; + public const int CMSG_CMS_RECIPIENT_INDEX_PARAM = 34; + public const int CMSG_CMS_RECIPIENT_ENCRYPTED_KEY_INDEX_PARAM = 35; + public const int CMSG_CMS_RECIPIENT_INFO_PARAM = 36; + public const int CMSG_UNPROTECTED_ATTR_PARAM = 37; + public const int CMSG_SIGNER_CERT_ID_PARAM = 38; + public const int CMSG_CMS_SIGNER_INFO_PARAM = 39; + + + //------------------------------------------------------------------------- + //dwObjectType for CryptQueryObject + //------------------------------------------------------------------------- + public const int CERT_QUERY_OBJECT_FILE = 0x00000001; + public const int CERT_QUERY_OBJECT_BLOB = 0x00000002; + + //------------------------------------------------------------------------- + //dwContentType for CryptQueryObject + //------------------------------------------------------------------------- + //encoded single certificate + public const int CERT_QUERY_CONTENT_CERT = 1; + //encoded single CTL + public const int CERT_QUERY_CONTENT_CTL = 2; + //encoded single CRL + public const int CERT_QUERY_CONTENT_CRL = 3; + //serialized store + public const int CERT_QUERY_CONTENT_SERIALIZED_STORE = 4; + //serialized single certificate + public const int CERT_QUERY_CONTENT_SERIALIZED_CERT = 5; + //serialized single CTL + public const int CERT_QUERY_CONTENT_SERIALIZED_CTL = 6; + //serialized single CRL + public const int CERT_QUERY_CONTENT_SERIALIZED_CRL = 7; + //a PKCS#7 signed message + public const int CERT_QUERY_CONTENT_PKCS7_SIGNED = 8; + //a PKCS#7 message, such as enveloped message. But it is not a signed message, + public const int CERT_QUERY_CONTENT_PKCS7_UNSIGNED = 9; + //a PKCS7 signed message embedded in a file + public const int CERT_QUERY_CONTENT_PKCS7_SIGNED_EMBED = 10; + //an encoded PKCS#10 + public const int CERT_QUERY_CONTENT_PKCS10 = 11; + //an encoded PKX BLOB + public const int CERT_QUERY_CONTENT_PFX = 12; + //an encoded CertificatePair (contains forward and/or reverse cross certs) + public const int CERT_QUERY_CONTENT_CERT_PAIR = 13; + + //------------------------------------------------------------------------- + //dwExpectedConentTypeFlags for CryptQueryObject + //------------------------------------------------------------------------- + //encoded single certificate + public const int CERT_QUERY_CONTENT_FLAG_CERT = (1 << CERT_QUERY_CONTENT_CERT); + + //encoded single CTL + public const int CERT_QUERY_CONTENT_FLAG_CTL = (1 << CERT_QUERY_CONTENT_CTL); + + //encoded single CRL + public const int CERT_QUERY_CONTENT_FLAG_CRL = (1 << CERT_QUERY_CONTENT_CRL); + + //serialized store + public const int CERT_QUERY_CONTENT_FLAG_SERIALIZED_STORE = (1 << CERT_QUERY_CONTENT_SERIALIZED_STORE); + + //serialized single certificate + public const int CERT_QUERY_CONTENT_FLAG_SERIALIZED_CERT = (1 << CERT_QUERY_CONTENT_SERIALIZED_CERT); + + //serialized single CTL + public const int CERT_QUERY_CONTENT_FLAG_SERIALIZED_CTL = (1 << CERT_QUERY_CONTENT_SERIALIZED_CTL); + + //serialized single CRL + public const int CERT_QUERY_CONTENT_FLAG_SERIALIZED_CRL = (1 << CERT_QUERY_CONTENT_SERIALIZED_CRL); + + //an encoded PKCS#7 signed message + public const int CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED = (1 << CERT_QUERY_CONTENT_PKCS7_SIGNED); + + //an encoded PKCS#7 message. But it is not a signed message + public const int CERT_QUERY_CONTENT_FLAG_PKCS7_UNSIGNED = (1 << CERT_QUERY_CONTENT_PKCS7_UNSIGNED); + + //the content includes an embedded PKCS7 signed message + public const int CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED = (1 << CERT_QUERY_CONTENT_PKCS7_SIGNED_EMBED); + + //an encoded PKCS#10 + public const int CERT_QUERY_CONTENT_FLAG_PKCS10 = (1 << CERT_QUERY_CONTENT_PKCS10); + + //an encoded PFX BLOB + public const int CERT_QUERY_CONTENT_FLAG_PFX = (1 << CERT_QUERY_CONTENT_PFX); + + //an encoded CertificatePair (contains forward and/or reverse cross certs) + public const int CERT_QUERY_CONTENT_FLAG_CERT_PAIR = (1 << CERT_QUERY_CONTENT_CERT_PAIR); + + //content can be any type + public const int CERT_QUERY_CONTENT_FLAG_ALL = + CERT_QUERY_CONTENT_FLAG_CERT | + CERT_QUERY_CONTENT_FLAG_CTL | + CERT_QUERY_CONTENT_FLAG_CRL | + CERT_QUERY_CONTENT_FLAG_SERIALIZED_STORE | + CERT_QUERY_CONTENT_FLAG_SERIALIZED_CERT | + CERT_QUERY_CONTENT_FLAG_SERIALIZED_CTL | + CERT_QUERY_CONTENT_FLAG_SERIALIZED_CRL | + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED | + CERT_QUERY_CONTENT_FLAG_PKCS7_UNSIGNED | + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED | + CERT_QUERY_CONTENT_FLAG_PKCS10 | + CERT_QUERY_CONTENT_FLAG_PFX | + CERT_QUERY_CONTENT_FLAG_CERT_PAIR; + + //------------------------------------------------------------------------- + //dwFormatType for CryptQueryObject + //------------------------------------------------------------------------- + //the content is in binary format + public const int CERT_QUERY_FORMAT_BINARY = 1; + + //the content is base64 encoded + public const int CERT_QUERY_FORMAT_BASE64_ENCODED = 2; + + //the content is ascii hex encoded with "{ASN}" prefix + public const int CERT_QUERY_FORMAT_ASN_ASCII_HEX_ENCODED = 3; + + //------------------------------------------------------------------------- + //dwExpectedFormatTypeFlags for CryptQueryObject + //------------------------------------------------------------------------- + //the content is in binary format + public const int CERT_QUERY_FORMAT_FLAG_BINARY = (1 << CERT_QUERY_FORMAT_BINARY); + + //the content is base64 encoded + public const int CERT_QUERY_FORMAT_FLAG_BASE64_ENCODED = (1 << CERT_QUERY_FORMAT_BASE64_ENCODED); + + //the content is ascii hex encoded with "{ASN}" prefix + public const int CERT_QUERY_FORMAT_FLAG_ASN_ASCII_HEX_ENCODED = (1 << CERT_QUERY_FORMAT_ASN_ASCII_HEX_ENCODED); + + //the content can be of any format + public const int CERT_QUERY_FORMAT_FLAG_ALL = + CERT_QUERY_FORMAT_FLAG_BINARY | + CERT_QUERY_FORMAT_FLAG_BASE64_ENCODED | + CERT_QUERY_FORMAT_FLAG_ASN_ASCII_HEX_ENCODED; + + + public static Uri GetSignatureUrl(string filename) + { + try + { + int encodingType; + int contentType; + int formatType; + IntPtr certStore = IntPtr.Zero; + IntPtr cryptMsg = IntPtr.Zero; + IntPtr context = IntPtr.Zero; + + if (!WinCrypt.CryptQueryObject(WinCrypt.CERT_QUERY_OBJECT_FILE, Marshal.StringToHGlobalUni(filename), WinCrypt.CERT_QUERY_CONTENT_FLAG_ALL, WinCrypt.CERT_QUERY_FORMAT_FLAG_ALL, 0, out encodingType, out contentType, out formatType, ref certStore, ref cryptMsg, ref context)) { return null; } + + // Get size of the encoded message. + int cbData = 0; + if (!WinCrypt.CryptMsgGetParam(cryptMsg, WinCrypt.CMSG_ENCODED_MESSAGE, 0, IntPtr.Zero, ref cbData)) { return null; } + var vData = new byte[cbData]; + + // Get the encoded message. + if (!WinCrypt.CryptMsgGetParam(cryptMsg, WinCrypt.CMSG_ENCODED_MESSAGE, 0, vData, ref cbData)) { return null; } + + var signedCms = new SignedCms(); + signedCms.Decode(vData); + + foreach (var signerInfo in signedCms.SignerInfos) + { + foreach (CryptographicAttributeObject signedAttribute in signerInfo.SignedAttributes) + { + if (signedAttribute.Oid.Value == "1.3.6.1.4.1.311.2.1.12") + { + foreach (AsnEncodedData x in signedAttribute.Values) + { + string z = x.Format(true); + int i = z.IndexOf("68 74 74 70 73 3a 2f 2f"); // "https://" + if (i >= 0) { return new Uri(System.Text.UTF8Encoding.UTF8.GetString(FromHex(z.Substring(i)))); } + } + } + } + } + } + catch (Exception) { } + return null; + } + + private static byte[] FromHex(string hex) + { + hex = hex.Replace(" ", ""); + byte[] raw = new byte[hex.Length / 2]; + for (int i = 0; i < raw.Length; i++) { raw[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); } + return raw; + } + + } +} \ No newline at end of file