CertHelper.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Security.Cryptography.X509Certificates;
  7. using System.Text;
  8. using Org.BouncyCastle.Asn1;
  9. using Org.BouncyCastle.Asn1.Pkcs;
  10. using Org.BouncyCastle.Asn1.X509;
  11. using Org.BouncyCastle.Crypto;
  12. using Org.BouncyCastle.Crypto.Generators;
  13. using Org.BouncyCastle.Crypto.Parameters;
  14. using Org.BouncyCastle.Crypto.Prng;
  15. using Org.BouncyCastle.Math;
  16. using Org.BouncyCastle.OpenSsl;
  17. using Org.BouncyCastle.Pkcs;
  18. using Org.BouncyCastle.Security;
  19. using Org.BouncyCastle.Utilities;
  20. using Org.BouncyCastle.X509;
  21. using Attribute = Org.BouncyCastle.Asn1.Cms.Attribute;
  22. using SysHashAlgorName = System.Security.Cryptography.HashAlgorithmName;
  23. using BcCertificate = Org.BouncyCastle.X509.X509Certificate;
  24. using X509Extension = Org.BouncyCastle.Asn1.X509.X509Extension;
  25. namespace PRSServer
  26. {
  27. /// <summary>
  28. /// Wrapper class around a native BouncyCastle Asymmetric Key Pair.
  29. /// </summary>
  30. public class CertPrivateKey
  31. {
  32. public AsymmetricCipherKeyPair KeyPair { get; set; }
  33. }
  34. /// <summary>
  35. /// Collection of static routines for working with basic entities needs to
  36. /// support X509 Certificate operations, including request generation,
  37. /// private key management, standards-based serialization and export.
  38. /// </summary>
  39. /// <remarks>
  40. /// Unfortunately there is not yet enough <i>out-of-the-box</i> support for
  41. /// general certificate management in .NET Standard, so we rely on a 3rd-party
  42. /// library to handle most of this work for us, in this case the very capable
  43. /// BouncyCastle C# library.
  44. /// </remarks>
  45. public static class CertHelper
  46. {
  47. // Useful references and examples for BC:
  48. // CSR:
  49. // http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation
  50. // https://gist.github.com/Venomed/5337717aadfb61b09e58
  51. // http://codereview.stackexchange.com/questions/84752/net-bouncycastle-csr-and-private-key-generation
  52. // Other:
  53. // https://www.txedo.com/blog/java-read-rsa-keys-pem-file/
  54. public const int RSA_BITS_DEFAULT = 2048;
  55. public const int RSA_BITS_MINIMUM = 1024 + 1; // LE no longer allows 1024-bit PrvKeys
  56. // This is based on the BC RSA Generator code:
  57. // https://github.com/bcgit/bc-csharp/blob/fba5af528ce7dcd0ac0513363196a62639b82a86/crypto/src/crypto/generators/RsaKeyPairGenerator.cs#L37
  58. private const int DEFAULT_CERTAINTY = 100;
  59. public static readonly BigInteger RSA_E_3 = BigInteger.Three;
  60. public static readonly BigInteger RSA_E_F4 = BigInteger.ValueOf(0x10001);
  61. public static CertPrivateKey GenerateRsaPrivateKey(int bits, string PubExp = null)
  62. {
  63. // Bits less than 1024 are weak Ref: http://openssl.org/docs/manmaster/crypto/RSA_generate_key_ex.html
  64. if (bits < RSA_BITS_MINIMUM)
  65. bits = RSA_BITS_DEFAULT;
  66. BigInteger e;
  67. if (string.IsNullOrEmpty(PubExp))
  68. e = RSA_E_F4;
  69. else if (PubExp.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
  70. e = new BigInteger(PubExp, 16);
  71. else
  72. e = new BigInteger(PubExp);
  73. var rsaKgp = new RsaKeyGenerationParameters(
  74. e, new SecureRandom(), bits, DEFAULT_CERTAINTY);
  75. var rkpg = new RsaKeyPairGenerator();
  76. rkpg.Init(rsaKgp);
  77. var ackp = rkpg.GenerateKeyPair();
  78. return new CertPrivateKey
  79. {
  80. KeyPair = ackp
  81. };
  82. }
  83. public static CertPrivateKey GenerateEcPrivateKey(int bits)
  84. {
  85. // From:
  86. // http://www.bouncycastle.org/wiki/display/JA1/Elliptic+Curve+Key+Pair+Generation+and+Key+Factories
  87. // var csr = new Pkcs10CertificationRequest()
  88. // string curveName;
  89. // switch (bits)
  90. // {
  91. // case 256:
  92. // curveName = "P-256";
  93. // break;
  94. // case 384:
  95. // curveName = "P-384";
  96. // break;
  97. // default:
  98. // throw new ArgumentException("bit length is unsupported or unknown", nameof(bits));
  99. // }
  100. // var ecParamSpec = ECNamedCurveTable.GetByName(curveName);
  101. // From:
  102. // https://www.codeproject.com/Tips/1150485/Csharp-Elliptical-Curve-Cryptography-with-Bouncy-C
  103. var ecKpg = new ECKeyPairGenerator("ECDSA");
  104. ecKpg.Init(new KeyGenerationParameters(new SecureRandom(), bits));
  105. var ecKp = ecKpg.GenerateKeyPair();
  106. return new CertPrivateKey
  107. {
  108. KeyPair = ecKp
  109. };
  110. }
  111. public static void ExportPrivateKey(CertPrivateKey pk, EncodingFormat fmt, Stream target)
  112. {
  113. switch (fmt)
  114. {
  115. case EncodingFormat.PEM:
  116. var pem = ToPrivatePem(pk.KeyPair);
  117. var bytes = Encoding.UTF8.GetBytes(pem);
  118. target.Write(bytes, 0, bytes.Length);
  119. break;
  120. case EncodingFormat.DER:
  121. var der = PrivateKeyInfoFactory.CreatePrivateKeyInfo(pk.KeyPair.Private).GetDerEncoded();
  122. target.Write(der, 0, der.Length);
  123. break;
  124. default:
  125. throw new NotSupportedException("unsupported encoding format");
  126. }
  127. }
  128. public static CertPrivateKey ImportPrivateKey(EncodingFormat fmt, Stream source)
  129. {
  130. if (fmt != EncodingFormat.PEM)
  131. throw new NotSupportedException("Unsupported archive format");
  132. using (var tr = new StreamReader(source))
  133. {
  134. var pr = new PemReader(tr);
  135. var pem = pr.ReadObject();
  136. var ackp = pem as AsymmetricCipherKeyPair;
  137. if (ackp != null)
  138. {
  139. if(ackp.Private is RsaPrivateCrtKeyParameters rsa || ackp.Private is ECPrivateKeyParameters ec)
  140. {
  141. return new CertPrivateKey
  142. {
  143. KeyPair = ackp
  144. };
  145. }
  146. }
  147. throw new InvalidDataException("could not read source as PEM private key");
  148. }
  149. }
  150. public static byte[] GenerateRsaCsr(IEnumerable<string> names,
  151. CertPrivateKey pk, SysHashAlgorName? hashAlgor = null)
  152. {
  153. if (hashAlgor == null)
  154. hashAlgor = SysHashAlgorName.SHA256;
  155. var attrs = new Dictionary<DerObjectIdentifier, string>
  156. {
  157. [X509Name.CN] = names.First()
  158. };
  159. var subj = new X509Name(attrs.Keys.ToList(), attrs.Values.ToList());
  160. var ackp = pk.KeyPair;
  161. var sigAlg = $"{hashAlgor.Value.Name}withRSA";
  162. var csrAttrs = new List<Asn1Encodable>();
  163. var gnames = new List<GeneralName>(
  164. names.Select(x => new GeneralName(GeneralName.DnsName, x)));
  165. var altNames = new GeneralNames(gnames.ToArray());
  166. #pragma warning disable CS0612 // Type or member is obsolete
  167. var x509Ext = new X509Extensions(new Hashtable
  168. {
  169. [X509Extensions.SubjectAlternativeName] = new X509Extension(
  170. false, new DerOctetString(altNames))
  171. });
  172. #pragma warning restore CS0612 // Type or member is obsolete
  173. csrAttrs.Add(new Attribute(
  174. PkcsObjectIdentifiers.Pkcs9AtExtensionRequest,
  175. new DerSet(x509Ext)));
  176. #pragma warning disable CS0618 // Type or member is obsolete
  177. var csr = new Pkcs10CertificationRequest(sigAlg,
  178. subj, ackp.Public, new DerSet(csrAttrs.ToArray()), ackp.Private);
  179. #pragma warning restore CS0618 // Type or member is obsolete
  180. return csr.GetDerEncoded();
  181. }
  182. public static byte[] GenerateEcCsr(IEnumerable<string> names,
  183. CertPrivateKey pk, SysHashAlgorName? hashAlgor = null)
  184. {
  185. if (hashAlgor == null)
  186. hashAlgor = SysHashAlgorName.SHA256;
  187. var attrs = new Dictionary<DerObjectIdentifier, string>
  188. {
  189. [X509Name.CN] = names.First()
  190. };
  191. var subj = new X509Name(attrs.Keys.ToList(), attrs.Values.ToList());
  192. var ackp = pk.KeyPair;
  193. var sigAlg = $"{hashAlgor.Value.Name}withECDSA";
  194. var csrAttrs = new List<Asn1Encodable>();
  195. var gnames = new List<GeneralName>(
  196. names.Select(x => new GeneralName(GeneralName.DnsName, x)));
  197. var altNames = new GeneralNames(gnames.ToArray());
  198. #pragma warning disable CS0612 // Type or member is obsolete
  199. var x509Ext = new X509Extensions(new Hashtable
  200. {
  201. [X509Extensions.SubjectAlternativeName] = new X509Extension(
  202. false, new DerOctetString(altNames))
  203. });
  204. #pragma warning restore CS0612 // Type or member is obsolete
  205. csrAttrs.Add(new Attribute(
  206. PkcsObjectIdentifiers.Pkcs9AtExtensionRequest,
  207. new DerSet(x509Ext)));
  208. #pragma warning disable CS0618 // Type or member is obsolete
  209. var csr = new Pkcs10CertificationRequest(sigAlg,
  210. subj, ackp.Public, new DerSet(csrAttrs.ToArray()), ackp.Private);
  211. #pragma warning restore CS0618 // Type or member is obsolete
  212. return csr.GetDerEncoded();
  213. }
  214. public static (CertPrivateKey, BcCertificate) GenerateRsaCACertificate(string subjectName, int keyStrength = 2048)
  215. {
  216. // Generating Random Numbers
  217. var randomGenerator = new CryptoApiRandomGenerator();
  218. var random = new SecureRandom(randomGenerator);
  219. // The Certificate Generator
  220. var certificateGenerator = new X509V3CertificateGenerator();
  221. // Serial Number
  222. var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
  223. certificateGenerator.SetSerialNumber(serialNumber);
  224. // Signature Algorithm
  225. const string signatureAlgorithm = "SHA256WithRSA";
  226. #pragma warning disable CS0618 // Type or member is obsolete
  227. certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm);
  228. #pragma warning restore CS0618 // Type or member is obsolete
  229. // Issuer and Subject Name
  230. var subjectDN = new X509Name(subjectName);
  231. var issuerDN = subjectDN;
  232. certificateGenerator.SetIssuerDN(issuerDN);
  233. certificateGenerator.SetSubjectDN(subjectDN);
  234. // Valid For
  235. var notBefore = DateTime.UtcNow.Date;
  236. var notAfter = notBefore.AddYears(2);
  237. certificateGenerator.SetNotBefore(notBefore);
  238. certificateGenerator.SetNotAfter(notAfter);
  239. // Subject Public Key
  240. AsymmetricCipherKeyPair subjectKeyPair;
  241. var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
  242. var keyPairGenerator = new RsaKeyPairGenerator();
  243. keyPairGenerator.Init(keyGenerationParameters);
  244. subjectKeyPair = keyPairGenerator.GenerateKeyPair();
  245. certificateGenerator.SetPublicKey(subjectKeyPair.Public);
  246. // Generating the Certificate
  247. var issuerKeyPair = subjectKeyPair;
  248. // selfsign certificate
  249. #pragma warning disable CS0618 // Type or member is obsolete
  250. var certificate = certificateGenerator.Generate(issuerKeyPair.Private, random);
  251. #pragma warning restore CS0618 // Type or member is obsolete
  252. // var x509 = new System.Security.Cryptography.X509Certificates.X509Certificate2(certificate.GetEncoded());
  253. // // Add CA certificate to Root store
  254. // addCertToStore(cert, StoreName.Root, StoreLocation.CurrentUser);
  255. var key = new CertPrivateKey
  256. {
  257. KeyPair = issuerKeyPair
  258. };
  259. return (key, certificate);
  260. }
  261. public static (CertPrivateKey, BcCertificate) GenerateRsaSelfSignedCertificate(string subjectName, string issuerName,
  262. AsymmetricKeyParameter issuerPrivKey, int keyStrength = 2048)
  263. {
  264. // Generating Random Numbers
  265. var randomGenerator = new CryptoApiRandomGenerator();
  266. var random = new SecureRandom(randomGenerator);
  267. // The Certificate Generator
  268. var certificateGenerator = new X509V3CertificateGenerator();
  269. // Serial Number
  270. var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
  271. certificateGenerator.SetSerialNumber(serialNumber);
  272. // Signature Algorithm
  273. const string signatureAlgorithm = "SHA256WithRSA";
  274. #pragma warning disable CS0618 // Type or member is obsolete
  275. certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm);
  276. #pragma warning restore CS0618 // Type or member is obsolete
  277. // Issuer and Subject Name
  278. var subjectDN = new X509Name(subjectName);
  279. var issuerDN = new X509Name(issuerName);
  280. certificateGenerator.SetIssuerDN(issuerDN);
  281. certificateGenerator.SetSubjectDN(subjectDN);
  282. // Valid For
  283. var notBefore = DateTime.UtcNow.AddMonths(-1).Date;
  284. var notAfter = DateTime.UtcNow.AddMonths(1).Date;
  285. certificateGenerator.SetNotBefore(notBefore);
  286. certificateGenerator.SetNotAfter(notAfter);
  287. // Subject Public Key
  288. AsymmetricCipherKeyPair subjectKeyPair;
  289. var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
  290. var keyPairGenerator = new RsaKeyPairGenerator();
  291. keyPairGenerator.Init(keyGenerationParameters);
  292. subjectKeyPair = keyPairGenerator.GenerateKeyPair();
  293. certificateGenerator.SetPublicKey(subjectKeyPair.Public);
  294. // Generating the Certificate
  295. var issuerKeyPair = subjectKeyPair;
  296. // selfsign certificate
  297. #pragma warning disable CS0618 // Type or member is obsolete
  298. var certificate = certificateGenerator.Generate(issuerPrivKey, random);
  299. #pragma warning restore CS0618 // Type or member is obsolete
  300. var key = new CertPrivateKey
  301. {
  302. KeyPair = subjectKeyPair
  303. };
  304. return (key, certificate);
  305. }
  306. public static X509Certificate2
  307. ToDotNetCert(CertPrivateKey key, BcCertificate certificate)
  308. {
  309. // merge into X509Certificate2
  310. // correcponding private key
  311. var info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(key.KeyPair.Private);
  312. var x509 = new X509Certificate2(
  313. certificate.GetEncoded());
  314. #pragma warning disable CS0618 // Type or member is obsolete
  315. var seq = (Asn1Sequence)Asn1Object.FromByteArray(info.PrivateKeyData.GetDerEncoded());
  316. #pragma warning restore CS0618 // Type or member is obsolete
  317. if (seq.Count != 9)
  318. throw new PemException("malformed sequence in RSA private key");
  319. #pragma warning disable CS0618 // Type or member is obsolete
  320. var rsa = new RsaPrivateKeyStructure(seq);
  321. #pragma warning restore CS0618 // Type or member is obsolete
  322. var rsaparams = new RsaPrivateCrtKeyParameters(
  323. rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent,
  324. rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2, rsa.Coefficient);
  325. x509.PrivateKey = DotNetUtilities.ToRSA(rsaparams);
  326. return x509;
  327. }
  328. public static BcCertificate ImportCertificate(EncodingFormat fmt, Stream source)
  329. {
  330. BcCertificate bcCert = null;
  331. if (fmt == EncodingFormat.DER)
  332. {
  333. var certParser = new X509CertificateParser();
  334. bcCert = certParser.ReadCertificate(source);
  335. }
  336. else if (fmt == EncodingFormat.PEM)
  337. {
  338. using (var tr = new StreamReader(source))
  339. {
  340. var pr = new PemReader(tr);
  341. bcCert = (BcCertificate)pr.ReadObject();
  342. }
  343. }
  344. else
  345. {
  346. throw new NotSupportedException("encoding format has not been implemented");
  347. }
  348. return bcCert;
  349. }
  350. public static void ExportCertificate(BcCertificate cert, EncodingFormat fmt, Stream target)
  351. {
  352. if (fmt == EncodingFormat.PEM)
  353. {
  354. using (var tw = new StringWriter())
  355. {
  356. var pw = new PemWriter(tw);
  357. pw.WriteObject(cert);
  358. var pemBytes = Encoding.UTF8.GetBytes(tw.GetStringBuilder().ToString());
  359. target.Write(pemBytes, 0, pemBytes.Length);
  360. }
  361. }
  362. else if (fmt == EncodingFormat.DER)
  363. {
  364. var der = cert.GetEncoded();
  365. target.Write(der, 0, der.Length);
  366. }
  367. else
  368. {
  369. throw new NotSupportedException("unsupported encoding format");
  370. }
  371. }
  372. public static void ExportArchive(CertPrivateKey pk, IEnumerable<BcCertificate> certs,
  373. ArchiveFormat fmt, Stream target, string password = null)
  374. {
  375. if (fmt == ArchiveFormat.PKCS12)
  376. {
  377. var bcCerts = certs.Select(x =>
  378. new X509CertificateEntry(x)).ToArray();
  379. var pfx = new Pkcs12Store();
  380. pfx.SetCertificateEntry(bcCerts[0].Certificate.SubjectDN.ToString(), bcCerts[0]);
  381. pfx.SetKeyEntry(bcCerts[0].Certificate.SubjectDN.ToString(),
  382. new AsymmetricKeyEntry(pk.KeyPair.Private), new[] { bcCerts[0] });
  383. for (var i = 1; i < bcCerts.Length; ++i)
  384. //pfx.SetCertificateEntry(bcCerts[i].Certificate.SubjectDN.ToString(),
  385. pfx.SetCertificateEntry(i.ToString(), bcCerts[i]);
  386. // It used to be pretty straight forward to export this...
  387. pfx.Save(target, password?.ToCharArray(), new SecureRandom());
  388. }
  389. else
  390. {
  391. throw new NotSupportedException("unsupported archive format");
  392. }
  393. }
  394. private static string ToPrivatePem(AsymmetricCipherKeyPair ackp)
  395. {
  396. string pem;
  397. using (var tw = new StringWriter())
  398. {
  399. var pw = new PemWriter(tw);
  400. pw.WriteObject(ackp.Private);
  401. pem = tw.GetStringBuilder().ToString();
  402. tw.GetStringBuilder().Clear();
  403. }
  404. return pem;
  405. }
  406. }
  407. }