2using System.Collections.Generic;
4using System.Security.Cryptography.X509Certificates;
5using System.Security.Cryptography;
7using System.Threading.Tasks;
10using System.Text.RegularExpressions;
11using Spectre.Console.Rendering;
18 public static class CertificateUtilities
26 public static void DisplayCertificate(params X509Certificate2[] certificates)
28 Grid grid =
new Grid();
29 grid.AddColumn(
new GridColumn().NoWrap());
30 grid.AddColumn(
new GridColumn().PadLeft(2));
33 foreach (var certificate
in certificates)
35 CertificateSubject subject =
new CertificateSubject(certificate);
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));
46 if (!
string.IsNullOrEmpty(subject.Email))
48 grid.AddRow(
" Email Address", subject.Email);
51 if (!
string.IsNullOrEmpty(subject.Organization))
53 grid.AddRow(
" Organization", subject.Organization);
56 if (!
string.IsNullOrEmpty(subject.OrganizationalUnit))
58 grid.AddRow(
" Organizational Unit", subject.OrganizationalUnit);
61 if (!
string.IsNullOrEmpty(subject.Locality))
63 grid.AddRow(
" Locality", subject.Locality);
66 if (!
string.IsNullOrEmpty(subject.State))
68 grid.AddRow(
" State", subject.State);
71 if (!
string.IsNullOrEmpty(subject.Country))
73 grid.AddRow(
" Country", subject.Country);
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:"));
81 if (subject.Unknown.Count > 0)
83 grid.AddRow(
new Text(
" Other Properties"),
new Text(
string.Join(
"\n", subject.Unknown.Select(x => $
"{x.Key}={x.Value}"))));
89 AnsiConsole.Write(grid);
90 AnsiConsole.WriteLine();
102 public static X509Certificate2 Import(
string filePath)
105 using (FileStream fs =
new FileStream(filePath, FileMode.Open, FileAccess.Read))
107 buffer =
new byte[fs.Length];
108 fs.Read(buffer, 0, buffer.Length);
111 return Import(buffer);
123 public static X509Certificate2 Import(
byte[] data)
125 X509Certificate2 certificate;
128 certificate = X509CertificateLoader.LoadCertificate(data);
130 certificate =
new X509Certificate2(data);
148 public static X509Certificate2Collection ImportPFX(
string filePath,
string? password =
null)
152 using (FileStream fs =
new FileStream(filePath, FileMode.Open, FileAccess.Read))
154 buffer =
new byte[fs.Length];
155 fs.Read(buffer, 0, buffer.Length);
158 return ImportPFX(buffer, password);
173 public static X509Certificate2Collection ImportPFX(
byte[] data,
string? password =
null)
175 X509Certificate2Collection collection =
new X509Certificate2Collection();
178 collection.AddRange(X509CertificateLoader.LoadPkcs12Collection(data, password, X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable));
180 collection.Import(data, password, X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable);
192 public static CertificateSubject GetSubjectFromUser()
194 string? commonName =
null;
196 while (
string.IsNullOrEmpty(commonName))
198 Console.Write(
"Common Name (CN): ");
199 commonName = Console.ReadLine();
202 Console.Write(
"Email (E) (optional): ");
203 string? email = Console.ReadLine();
205 Console.Write(
"Organization (O) (optional): ");
206 string? organization = Console.ReadLine();
208 Console.Write(
"Organizational Unit (OU) (optional): ");
209 string? organizationalUnit = Console.ReadLine();
211 Console.Write(
"Locality (L) (optional): ");
212 string? locality = Console.ReadLine();
214 Console.Write(
"State or Province (ST) (optional): ");
215 string? state = Console.ReadLine();
217 Console.Write(
"Country (C) (optional): ");
218 string? country = Console.ReadLine();
220 return new CertificateSubject(commonName: commonName,
222 organization: organization,
223 organizationalUnit: organizationalUnit,
236 public static X509Certificate2Collection GetCertificates(
string pfxFilePath,
string pfxFilePassword,
bool pfxNoPasswordPrompt)
238 X509Certificate2Collection collection;
240 if (!
string.IsNullOrEmpty(pfxFilePath))
242 collection = LoadCertificatesFromPfx(pfxFilePath, pfxFilePassword, pfxNoPasswordPrompt);
248 X509Store store =
new X509Store(
"MY", StoreLocation.CurrentUser);
249 store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
251 collection = store.Certificates;
277 private static X509Certificate2Collection LoadCertificatesFromPfx(
string pfxFilePath,
string? pfxFilePassword,
bool pfxNoPasswordPrompt)
279 X509Certificate2Collection collection = [];
281 string pfpass = !
string.IsNullOrEmpty(pfxFilePassword) ? pfxFilePassword : !pfxNoPasswordPrompt ? Utilities.SecurePrompt(
"Enter PFX File password (if needed): ") :
"";
283 X509Certificate2Collection tempCollection = ImportPFX(pfxFilePath, pfpass);
285 IEnumerable<X509Certificate2> cond = tempCollection.Where(x => x.HasPrivateKey);
288 collection.AddRange(cond.ToArray());
292 collection.AddRange(tempCollection);
303 public static X509Certificate2 CreateSelfSignedCACertificate(
string subjectName)
305 using (RSA rsa = RSA.Create(4096))
308 var caRequest =
new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
311 caRequest.CertificateExtensions.Add(
312 new X509BasicConstraintsExtension(
true,
false, 0,
true));
315 caRequest.CertificateExtensions.Add(
316 new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign,
true));
319 caRequest.CertificateExtensions.Add(
320 new X509SubjectKeyIdentifierExtension(caRequest.PublicKey,
false));
323 var rootCert = caRequest.CreateSelfSigned(DateTimeOffset.UtcNow,
324 DateTimeOffset.UtcNow.AddYears(100));
327 var cert = ImportPFX(rootCert.Export(X509ContentType.Pfx)).Single();
339 public static X509Certificate2 IssueCertificate(
string subjectName, X509Certificate2 caCert)
341 using (RSA rsa = RSA.Create(2048))
343 _ = rsa.ExportRSAPrivateKey();
346 var req =
new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
349 req.CertificateExtensions.Add(
350 new X509BasicConstraintsExtension(
false,
false, 0,
false));
353 req.CertificateExtensions.Add(
354 new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
true));
357 req.CertificateExtensions.Add(
358 new X509SubjectKeyIdentifierExtension(req.PublicKey,
false));
361 byte[] serialNumber =
new byte[16];
362 using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
364 rng.GetBytes(serialNumber);
369 using (RSA? caPrivateKey = caCert.GetRSAPrivateKey())
371 if (caPrivateKey ==
null)
373 throw new InvalidOperationException(
"The provided CA certificate does not contain a private key.");
377 var issuedCert = req.Create(caCert, DateTimeOffset.UtcNow,
378 DateTimeOffset.UtcNow.AddYears(20), serialNumber);
380 return issuedCert.CopyWithPrivateKey(rsa);