-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathRdpSigner.cs
More file actions
353 lines (287 loc) · 11.2 KB
/
Copy pathRdpSigner.cs
File metadata and controls
353 lines (287 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
namespace NullOps.RdpSigner
{
/// <summary>
/// The magical Rdp Signer utility.
/// </summary>
public class RdpSigner : IDisposable
{
/// <summary>
/// Because Rdp expects windows new line formatting, we hardcode it.
/// </summary>
private const string WindowsNewLine = "\r\n";
/// <summary>
/// Digest Algorithm to request when CMS signing
/// </summary>
private const string RdpSignatureDigestAlgorithmName = "sha256";
/// <summary>
/// As it says on the box, two bytes with 0x0 value.
/// These are used to pad the end of the byte data that is signed.
/// The reason there is 2? Unicode!
/// </summary>
private static readonly byte[] TwoBlankBytes = { 0x0, 0x0 };
/// <summary>
/// The magical unknown rdp signature header.
/// </summary>
private static readonly byte[] SignatureHeader = { 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x00, 0x00 };
/// <summary>
/// The certificate to sign with
/// </summary>
private readonly X509Certificate2 m_signingCertificate;
/// <summary>
/// If we are already disposed
/// </summary>
private bool m_disposed = false;
/// <summary>
/// RdpSigner
/// </summary>
/// <param name="signingCertificate">The certificate you wish to use when signing</param>
public RdpSigner(X509Certificate2 signingCertificate)
{
m_signingCertificate = signingCertificate;
}
/// <summary>
/// Signs the provided rdp string
/// </summary>
/// <param name="rdpText">The string data inside an rdp file</param>
/// <returns>The signed value within an rdp file</returns>
public string Sign(string rdpText)
{
return Sign(rdpText, m_signingCertificate);
}
/// <summary>
/// Signs the rdp file at the given location
/// </summary>
/// <param name="rdpFilePath">Path to the rdp file</param>
public void SignFile(string rdpFilePath)
{
SignFile(rdpFilePath, m_signingCertificate);
}
/// <summary>
/// Signs the rdp file at the given location, outputting the signed file to a now location
/// </summary>
/// <param name="sourceRdpFilePath">The path to the source rdp file to sign</param>
/// <param name="outputRdpFilePath">The output path for the signed rdp file</param>
public void SignFile(string sourceRdpFilePath, string outputRdpFilePath)
{
SignFile(sourceRdpFilePath, outputRdpFilePath, m_signingCertificate);
}
/// <summary>
/// Signs the rdp file at the given location
/// </summary>
/// <param name="rdpFilePath">Path to the rdp file</param>
/// <param name="signingCertificate">The certificate you wish to use when signing</param>
public static void SignFile(string rdpFilePath, X509Certificate2 signingCertificate)
{
if (string.IsNullOrWhiteSpace(rdpFilePath))
{
throw new ArgumentException(nameof(rdpFilePath) + " must be a valid path.");
}
SignFile(rdpFilePath, rdpFilePath, signingCertificate);
}
/// <summary>
/// Signs the rdp file at the given location, outputting the signed file to a now location
/// </summary>
/// <param name="sourceRdpFilePath">The path to the source rdp file to sign</param>
/// <param name="outputRdpFilePath">The output path for the signed rdp file</param>
/// <param name="signingCertificate">The certificate you wish to use when signing</param>
public static void SignFile(string sourceRdpFilePath, string outputRdpFilePath, X509Certificate2 signingCertificate)
{
if (string.IsNullOrWhiteSpace(sourceRdpFilePath))
{
throw new ArgumentException(nameof(sourceRdpFilePath) + " must be a valid path.");
}
if (string.IsNullOrWhiteSpace(outputRdpFilePath))
{
throw new ArgumentException(nameof(outputRdpFilePath) + " must be a valid path.");
}
if (!File.Exists(sourceRdpFilePath))
{
throw new ArgumentException("File '" + sourceRdpFilePath + "' does not exist.");
}
string sourceText = File.ReadAllText(sourceRdpFilePath);
string signedText = Sign(sourceText, signingCertificate);
if (!string.IsNullOrWhiteSpace(signedText))
{
File.WriteAllText(outputRdpFilePath, signedText, Encoding.Unicode);
}
}
/// <summary>
/// Signs the provided rdp string
/// </summary>
/// <param name="rdpText">The string data inside an rdp file</param>
/// <param name="signingCertificate">The certificate you wish to use when signing</param>
/// <returns>The signed value within an rdp file</returns>
public static string Sign(string rdpText, X509Certificate2 signingCertificate)
{
List<RdpSetting> settings = ParseSettings(rdpText);
RemoveSignSettings(settings);
EnsureAlternateFullAddressExists(settings);
List<RdpSetting> settingsToSign = settings.Where(setting => setting.IsSignableSetting).ToList();
RdpSetting signScopeSetting = BuildSignScopeSetting(settingsToSign);
settings.Add(signScopeSetting);
settingsToSign.Add(signScopeSetting);
RdpSetting signatureSetting = BuildSignatureSetting(settingsToSign, signingCertificate);
settings.Add(signatureSetting);
string signedRdpFile = FlattenSettings(settings);
return signedRdpFile;
}
/// <summary>
/// Flattens a set of settings into their rdp file equivilent
/// </summary>
/// <param name="settings">The IEnumerable of settings to flatten</param>
/// <returns>The provided settings as a string in rdp file format</returns>
private static string FlattenSettings(List<RdpSetting> settings)
{
StringBuilder builder = new StringBuilder();
foreach (RdpSetting setting in settings)
{
builder.Append(setting).Append(WindowsNewLine);
}
return builder.ToString();
}
/// <summary>
/// Builds a signature setting for the provided settings
/// </summary>
/// <param name="settings">The settings to generate the signature for</param>
/// <param name="signingCertificate">The certificate to use to sign it</param>
/// <returns>The rdp signature setting</returns>
private static RdpSetting BuildSignatureSetting(List<RdpSetting> settings, X509Certificate2 signingCertificate)
{
string textToSign = FlattenSettings(settings);
byte[] bytesToSign = Encoding.Unicode.GetBytes(textToSign).Concat(TwoBlankBytes).ToArray();
byte[] settingSignature = SignBytes(bytesToSign, signingCertificate);
uint settingSignatureLength = Convert.ToUInt32(settingSignature.Length);
byte[] settingSignatureLengthBytes = BitConverter.GetBytes(settingSignatureLength);
byte[] signature = SignatureHeader.Concat(settingSignatureLengthBytes).Concat(settingSignature).ToArray();
string base64Signature = Convert.ToBase64String(signature);
// Add the silly little spaces every 64 characters that rdpsign.exe adds:
base64Signature = Regex.Replace(base64Signature, ".{64}", "$0 ");
return new RdpSetting(RdpSettingHelper.SignatureSettingName, RdpSettingHelper.SignatureSettingType, base64Signature);
}
/// <summary>
/// Signs a payload, using CMS, with the provided certificate
/// </summary>
/// <param name="payload">The bytes to sign</param>
/// <param name="signingCertificate">The certificate to sign with</param>
/// <returns>The signature</returns>
private static byte[] SignBytes(byte[] payload, X509Certificate2 signingCertificate)
{
CmsSigner signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, signingCertificate)
{
IncludeOption = X509IncludeOption.WholeChain,
DigestAlgorithm = Oid.FromFriendlyName(RdpSignatureDigestAlgorithmName, OidGroup.All)
};
ContentInfo content = new ContentInfo(payload);
SignedCms cmsPayload = new SignedCms(content, true);
cmsPayload.ComputeSignature(signer);
return cmsPayload.Encode();
}
/// <summary>
/// Builds the sign scope setting for the provided settings
/// </summary>
/// <param name="settings">The settings to scope for</param>
/// <returns>The sign scope setting containing all scopes</returns>
private static RdpSetting BuildSignScopeSetting(List<RdpSetting> settings)
{
StringBuilder scopeBuilder = new StringBuilder();
foreach (RdpSetting setting in settings)
{
if (!RdpSettingHelper.SecureSettingScopes.ContainsKey(setting.SettingName))
{
throw new ArgumentException("Setting '" + setting.SettingName + "' has no known sign scope. (Are you sure it is signable?)");
}
scopeBuilder.Append(RdpSettingHelper.SecureSettingScopes[setting.SettingName]).Append(',');
}
if (scopeBuilder.Length <= 0)
{
return null;
}
// Remove final comma
scopeBuilder.Length--;
string settingValue = scopeBuilder.ToString();
return new RdpSetting(RdpSettingHelper.SignScopeSettingName, RdpSettingHelper.SignScopeSettingType, settingValue);
}
/// <summary>
/// Checks if an alternate full address is supplied, if not, then all it.
/// This stops people from adding a full address after the signing is done
/// </summary>
/// <param name="settings">The collection of settings to check and add to</param>
private static void EnsureAlternateFullAddressExists(List<RdpSetting> settings)
{
if (settings.Any(x => x.SettingName.Equals(RdpSettingHelper.AlternateFullAddressSettingName, StringComparison.OrdinalIgnoreCase)))
{
return;
}
RdpSetting fullAddressSetting = settings.FirstOrDefault(x =>
x.SettingName.Equals(RdpSettingHelper.FullAddressSettingName, StringComparison.OrdinalIgnoreCase)
);
if (fullAddressSetting == null)
{
throw new Exception("Supplied Rdp settings do not contain a '" + RdpSettingHelper.FullAddressSettingName + "' setting");
}
settings.Add(new RdpSetting(RdpSettingHelper.AlternateFullAddressSettingName, fullAddressSetting.SettingType, fullAddressSetting.SettingValue));
}
/// <summary>
/// Removes any existing signatures and sign scopes
/// </summary>
/// <param name="settings">The collection of settings to check</param>
private static void RemoveSignSettings(List<RdpSetting> settings)
{
settings.RemoveAll(setting =>
setting.SettingName.Equals(RdpSettingHelper.SignScopeSettingName, StringComparison.OrdinalIgnoreCase) ||
setting.SettingName.Equals(RdpSettingHelper.SignatureSettingName, StringComparison.OrdinalIgnoreCase)
);
}
/// <summary>
/// Parses a string and converts each line into an rdp setting if it is valid
/// </summary>
/// <param name="data">The string to parse</param>
/// <returns>A list of rdp settings</returns>
private static List<RdpSetting> ParseSettings(string data)
{
List<RdpSetting> settings = new List<RdpSetting>();
if (string.IsNullOrWhiteSpace(data))
{
return settings;
}
using (StringReader reader = new StringReader(data))
{
string line;
while ((line = reader.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
RdpSetting parsedSetting;
if (RdpSetting.TryParse(line, out parsedSetting))
{
settings.Add(parsedSetting);
}
}
}
return settings;
}
/// <summary>
/// Disposes of this rdp signer.
/// </summary>
public void Dispose()
{
if (m_disposed)
{
return;
}
m_disposed = true;
m_signingCertificate.Dispose();
}
}
}