CertHelper.cs 19 KB

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