EasySign BETA
Digital Signing Tool
Loading...
Searching...
No Matches
CertificateUtilities.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Security.Cryptography.X509Certificates;
5using System.Security.Cryptography;
6using System.Text;
7using System.Threading.Tasks;
8using EnsureThat;
9using Spectre.Console;
10using System.Text.RegularExpressions;
11using Spectre.Console.Rendering;
12
14{
18 public static class CertificateUtilities
19 {
26 public static void DisplayCertificate(params X509Certificate2[] certificates)
27 {
28 Grid grid = new Grid();
29 grid.AddColumn(new GridColumn().NoWrap());
30 grid.AddColumn(new GridColumn().PadLeft(2));
31
32 int index = 1;
33 foreach (var certificate in certificates)
34 {
35 CertificateSubject subject = new CertificateSubject(certificate);
36
37 if (index > 1)
38 {
39 grid.AddRow();
40 }
41
42 grid.AddRow($"Certificate {(certificates.Length > 1 ? $"#{index}" : "Info")}:");
43 grid.AddRow(" Common Name", subject.CommonName);
44 grid.AddRow(" Issuer Name", certificate.GetNameInfo(X509NameType.SimpleName, true));
45
46 if (!string.IsNullOrEmpty(subject.Email))
47 {
48 grid.AddRow(" Email Address", subject.Email);
49 }
50
51 if (!string.IsNullOrEmpty(subject.Organization))
52 {
53 grid.AddRow(" Organization", subject.Organization);
54 }
55
56 if (!string.IsNullOrEmpty(subject.OrganizationalUnit))
57 {
58 grid.AddRow(" Organizational Unit", subject.OrganizationalUnit);
59 }
60
61 if (!string.IsNullOrEmpty(subject.Locality))
62 {
63 grid.AddRow(" Locality", subject.Locality);
64 }
65
66 if (!string.IsNullOrEmpty(subject.State))
67 {
68 grid.AddRow(" State", subject.State);
69 }
70
71 if (!string.IsNullOrEmpty(subject.Country))
72 {
73 grid.AddRow(" Country", subject.Country);
74 }
75
76 grid.AddRow(" Valid From", certificate.GetEffectiveDateString());
77 grid.AddRow(" Valid To", certificate.GetExpirationDateString());
78 grid.AddRow(" Thumbprint", Regex.Replace(certificate.Thumbprint, "(.{2})(?!$)", "$1:"));
79 grid.AddRow(" Serial Number", Regex.Replace(certificate.SerialNumber, "(.{2})(?!$)", "$1:"));
80
81 if (subject.Unknown.Count > 0)
82 {
83 grid.AddRow(new Text(" Other Properties"), new Text(string.Join("\n", subject.Unknown.Select(x => $"{x.Key}={x.Value}"))));
84 }
85
86 index++;
87 }
88
89 AnsiConsole.Write(grid);
90 AnsiConsole.WriteLine();
91 }
92
102 public static X509Certificate2 Import(string filePath)
103 {
104 byte[] buffer;
105 using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
106 {
107 buffer = new byte[fs.Length];
108 fs.Read(buffer, 0, buffer.Length);
109 }
110
111 return Import(buffer);
112 }
113
123 public static X509Certificate2 Import(byte[] data)
124 {
125 X509Certificate2 certificate;
126
127#if NET9_0_OR_GREATER
128 certificate = X509CertificateLoader.LoadCertificate(data);
129#else
130 certificate = new X509Certificate2(data);
131#endif
132
133 return certificate;
134 }
135
148 public static X509Certificate2Collection ImportPFX(string filePath, string? password = null)
149 {
150 byte[] buffer;
151
152 using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
153 {
154 buffer = new byte[fs.Length];
155 fs.Read(buffer, 0, buffer.Length);
156 }
157
158 return ImportPFX(buffer, password);
159 }
160
173 public static X509Certificate2Collection ImportPFX(byte[] data, string? password = null)
174 {
175 X509Certificate2Collection collection = new X509Certificate2Collection();
176
177#if NET9_0_OR_GREATER
178 collection.AddRange(X509CertificateLoader.LoadPkcs12Collection(data, password, X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable));
179#else
180 collection.Import(data, password, X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable);
181#endif
182
183 return collection;
184 }
185
192 public static CertificateSubject GetSubjectFromUser()
193 {
194 string? commonName = null;
195
196 while (string.IsNullOrEmpty(commonName))
197 {
198 Console.Write("Common Name (CN): ");
199 commonName = Console.ReadLine();
200 }
201
202 Console.Write("Email (E) (optional): ");
203 string? email = Console.ReadLine();
204
205 Console.Write("Organization (O) (optional): ");
206 string? organization = Console.ReadLine();
207
208 Console.Write("Organizational Unit (OU) (optional): ");
209 string? organizationalUnit = Console.ReadLine();
210
211 Console.Write("Locality (L) (optional): ");
212 string? locality = Console.ReadLine();
213
214 Console.Write("State or Province (ST) (optional): ");
215 string? state = Console.ReadLine();
216
217 Console.Write("Country (C) (optional): ");
218 string? country = Console.ReadLine();
219
220 return new CertificateSubject(commonName: commonName,
221 email: email,
222 organization: organization,
223 organizationalUnit: organizationalUnit,
224 locality: locality,
225 state: state,
226 country: country);
227 }
228
236 public static X509Certificate2Collection GetCertificates(string pfxFilePath, string pfxFilePassword, bool pfxNoPasswordPrompt)
237 {
238 X509Certificate2Collection collection;
239
240 if (!string.IsNullOrEmpty(pfxFilePath))
241 {
242 collection = LoadCertificatesFromPfx(pfxFilePath, pfxFilePassword, pfxNoPasswordPrompt);
243 }
244 else
245 {
246 try
247 {
248 X509Store store = new X509Store("MY", StoreLocation.CurrentUser);
249 store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
250
251 collection = store.Certificates;
252
253 store.Close();
254 }
255 catch
256 {
257 collection = [];
258 }
259 }
260
261 return collection;
262 }
263
277 private static X509Certificate2Collection LoadCertificatesFromPfx(string pfxFilePath, string? pfxFilePassword, bool pfxNoPasswordPrompt)
278 {
279 X509Certificate2Collection collection = [];
280
281 string pfpass = !string.IsNullOrEmpty(pfxFilePassword) ? pfxFilePassword : !pfxNoPasswordPrompt ? Utilities.SecurePrompt("Enter PFX File password (if needed): ") : "";
282
283 X509Certificate2Collection tempCollection = ImportPFX(pfxFilePath, pfpass);
284
285 IEnumerable<X509Certificate2> cond = tempCollection.Where(x => x.HasPrivateKey);
286 if (cond.Any())
287 {
288 collection.AddRange(cond.ToArray());
289 }
290 else
291 {
292 collection.AddRange(tempCollection);
293 }
294
295 return collection;
296 }
297
303 public static X509Certificate2 CreateSelfSignedCACertificate(string subjectName)
304 {
305 using (RSA rsa = RSA.Create(4096))
306 {
307 // Build the certificate request for the CA.
308 var caRequest = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
309
310 // Mark this certificate as a Certificate Authority with the Basic Constraints extension.
311 caRequest.CertificateExtensions.Add(
312 new X509BasicConstraintsExtension(true, false, 0, true));
313
314 // Set key usages to allow certificate signing and CRL signing.
315 caRequest.CertificateExtensions.Add(
316 new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
317
318 // Add a Subject Key Identifier.
319 caRequest.CertificateExtensions.Add(
320 new X509SubjectKeyIdentifierExtension(caRequest.PublicKey, false));
321
322 // Create the self-signed certificate. Validity is set from now to 100 years in the future.
323 var rootCert = caRequest.CreateSelfSigned(DateTimeOffset.UtcNow,
324 DateTimeOffset.UtcNow.AddYears(100));
325
326 // Export and re-import to mark the key as exportable (if needed for further signing).
327 var cert = ImportPFX(rootCert.Export(X509ContentType.Pfx)).Single();
328
329 return cert;
330 }
331 }
332
339 public static X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 caCert)
340 {
341 using (RSA rsa = RSA.Create(2048))
342 {
343 _ = rsa.ExportRSAPrivateKey(); // Ensure the RSA key is created.
344
345 // Build the certificate request for the issued certificate.
346 var req = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
347
348 // This certificate is not a CA, so basic constraints are set accordingly.
349 req.CertificateExtensions.Add(
350 new X509BasicConstraintsExtension(false, false, 0, false));
351
352 // Use key usage flags appropriate for, e.g., a server certificate.
353 req.CertificateExtensions.Add(
354 new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true));
355
356 // Add a Subject Key Identifier.
357 req.CertificateExtensions.Add(
358 new X509SubjectKeyIdentifierExtension(req.PublicKey, false));
359
360 // Generate a random serial number.
361 byte[] serialNumber = new byte[16];
362 using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
363 {
364 rng.GetBytes(serialNumber);
365 }
366
367 // Sign the new certificate with the CA certificate.
368 // Note: The CA certificate must contain its private key for signing.
369 using (RSA? caPrivateKey = caCert.GetRSAPrivateKey())
370 {
371 if (caPrivateKey == null)
372 {
373 throw new InvalidOperationException("The provided CA certificate does not contain a private key.");
374 }
375
376 // Create the certificate valid from now until 20 years in the future.
377 var issuedCert = req.Create(caCert, DateTimeOffset.UtcNow,
378 DateTimeOffset.UtcNow.AddYears(20), serialNumber);
379
380 return issuedCert.CopyWithPrivateKey(rsa);
381 }
382 }
383 }
384 }
385}