CertHelper.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  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. var rsa = ackp.Private as RsaPrivateCrtKeyParameters;
  140. if (rsa != null)
  141. return new CertPrivateKey
  142. {
  143. KeyPair = ackp
  144. };
  145. }
  146. throw new InvalidDataException("could not read source as PEM private key");
  147. }
  148. }
  149. public static byte[] GenerateRsaCsr(IEnumerable<string> names,
  150. CertPrivateKey pk, SysHashAlgorName? hashAlgor = null)
  151. {
  152. if (hashAlgor == null)
  153. hashAlgor = SysHashAlgorName.SHA256;
  154. var attrs = new Dictionary<DerObjectIdentifier, string>
  155. {
  156. [X509Name.CN] = names.First()
  157. };
  158. var subj = new X509Name(attrs.Keys.ToList(), attrs.Values.ToList());
  159. var ackp = pk.KeyPair;
  160. var sigAlg = $"{hashAlgor.Value.Name}withRSA";
  161. var csrAttrs = new List<Asn1Encodable>();
  162. var gnames = new List<GeneralName>(
  163. names.Select(x => new GeneralName(GeneralName.DnsName, x)));
  164. var altNames = new GeneralNames(gnames.ToArray());
  165. #pragma warning disable CS0612 // Type or member is obsolete
  166. var x509Ext = new X509Extensions(new Hashtable
  167. {
  168. [X509Extensions.SubjectAlternativeName] = new X509Extension(
  169. false, new DerOctetString(altNames))
  170. });
  171. #pragma warning restore CS0612 // Type or member is obsolete
  172. csrAttrs.Add(new Attribute(
  173. PkcsObjectIdentifiers.Pkcs9AtExtensionRequest,
  174. new DerSet(x509Ext)));
  175. #pragma warning disable CS0618 // Type or member is obsolete
  176. var csr = new Pkcs10CertificationRequest(sigAlg,
  177. subj, ackp.Public, new DerSet(csrAttrs.ToArray()), ackp.Private);
  178. #pragma warning restore CS0618 // Type or member is obsolete
  179. return csr.GetDerEncoded();
  180. }
  181. public static byte[] GenerateEcCsr(IEnumerable<string> names,
  182. CertPrivateKey pk, SysHashAlgorName? hashAlgor = null)
  183. {
  184. if (hashAlgor == null)
  185. hashAlgor = SysHashAlgorName.SHA256;
  186. var attrs = new Dictionary<DerObjectIdentifier, string>
  187. {
  188. [X509Name.CN] = names.First()
  189. };
  190. var subj = new X509Name(attrs.Keys.ToList(), attrs.Values.ToList());
  191. var ackp = pk.KeyPair;
  192. var sigAlg = $"{hashAlgor.Value.Name}withECDSA";
  193. var csrAttrs = new List<Asn1Encodable>();
  194. var gnames = new List<GeneralName>(
  195. names.Select(x => new GeneralName(GeneralName.DnsName, x)));
  196. var altNames = new GeneralNames(gnames.ToArray());
  197. #pragma warning disable CS0612 // Type or member is obsolete
  198. var x509Ext = new X509Extensions(new Hashtable
  199. {
  200. [X509Extensions.SubjectAlternativeName] = new X509Extension(
  201. false, new DerOctetString(altNames))
  202. });
  203. #pragma warning restore CS0612 // Type or member is obsolete
  204. csrAttrs.Add(new Attribute(
  205. PkcsObjectIdentifiers.Pkcs9AtExtensionRequest,
  206. new DerSet(x509Ext)));
  207. #pragma warning disable CS0618 // Type or member is obsolete
  208. var csr = new Pkcs10CertificationRequest(sigAlg,
  209. subj, ackp.Public, new DerSet(csrAttrs.ToArray()), ackp.Private);
  210. #pragma warning restore CS0618 // Type or member is obsolete
  211. return csr.GetDerEncoded();
  212. }
  213. public static (CertPrivateKey, BcCertificate) GenerateRsaCACertificate(string subjectName, int keyStrength = 2048)
  214. {
  215. // Generating Random Numbers
  216. var randomGenerator = new CryptoApiRandomGenerator();
  217. var random = new SecureRandom(randomGenerator);
  218. // The Certificate Generator
  219. var certificateGenerator = new X509V3CertificateGenerator();
  220. // Serial Number
  221. var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
  222. certificateGenerator.SetSerialNumber(serialNumber);
  223. // Signature Algorithm
  224. const string signatureAlgorithm = "SHA256WithRSA";
  225. #pragma warning disable CS0618 // Type or member is obsolete
  226. certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm);
  227. #pragma warning restore CS0618 // Type or member is obsolete
  228. // Issuer and Subject Name
  229. var subjectDN = new X509Name(subjectName);
  230. var issuerDN = subjectDN;
  231. certificateGenerator.SetIssuerDN(issuerDN);
  232. certificateGenerator.SetSubjectDN(subjectDN);
  233. // Valid For
  234. var notBefore = DateTime.UtcNow.Date;
  235. var notAfter = notBefore.AddYears(2);
  236. certificateGenerator.SetNotBefore(notBefore);
  237. certificateGenerator.SetNotAfter(notAfter);
  238. // Subject Public Key
  239. AsymmetricCipherKeyPair subjectKeyPair;
  240. var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
  241. var keyPairGenerator = new RsaKeyPairGenerator();
  242. keyPairGenerator.Init(keyGenerationParameters);
  243. subjectKeyPair = keyPairGenerator.GenerateKeyPair();
  244. certificateGenerator.SetPublicKey(subjectKeyPair.Public);
  245. // Generating the Certificate
  246. var issuerKeyPair = subjectKeyPair;
  247. // selfsign certificate
  248. #pragma warning disable CS0618 // Type or member is obsolete
  249. var certificate = certificateGenerator.Generate(issuerKeyPair.Private, random);
  250. #pragma warning restore CS0618 // Type or member is obsolete
  251. // var x509 = new System.Security.Cryptography.X509Certificates.X509Certificate2(certificate.GetEncoded());
  252. // // Add CA certificate to Root store
  253. // addCertToStore(cert, StoreName.Root, StoreLocation.CurrentUser);
  254. var key = new CertPrivateKey
  255. {
  256. KeyPair = issuerKeyPair
  257. };
  258. return (key, certificate);
  259. }
  260. public static (CertPrivateKey, BcCertificate) GenerateRsaSelfSignedCertificate(string subjectName, string issuerName,
  261. AsymmetricKeyParameter issuerPrivKey, int keyStrength = 2048)
  262. {
  263. // Generating Random Numbers
  264. var randomGenerator = new CryptoApiRandomGenerator();
  265. var random = new SecureRandom(randomGenerator);
  266. // The Certificate Generator
  267. var certificateGenerator = new X509V3CertificateGenerator();
  268. // Serial Number
  269. var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
  270. certificateGenerator.SetSerialNumber(serialNumber);
  271. // Signature Algorithm
  272. const string signatureAlgorithm = "SHA256WithRSA";
  273. #pragma warning disable CS0618 // Type or member is obsolete
  274. certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm);
  275. #pragma warning restore CS0618 // Type or member is obsolete
  276. // Issuer and Subject Name
  277. var subjectDN = new X509Name(subjectName);
  278. var issuerDN = new X509Name(issuerName);
  279. certificateGenerator.SetIssuerDN(issuerDN);
  280. certificateGenerator.SetSubjectDN(subjectDN);
  281. // Valid For
  282. var notBefore = DateTime.UtcNow.AddMonths(-1).Date;
  283. var notAfter = DateTime.UtcNow.AddMonths(1).Date;
  284. certificateGenerator.SetNotBefore(notBefore);
  285. certificateGenerator.SetNotAfter(notAfter);
  286. // Subject Public Key
  287. AsymmetricCipherKeyPair subjectKeyPair;
  288. var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
  289. var keyPairGenerator = new RsaKeyPairGenerator();
  290. keyPairGenerator.Init(keyGenerationParameters);
  291. subjectKeyPair = keyPairGenerator.GenerateKeyPair();
  292. certificateGenerator.SetPublicKey(subjectKeyPair.Public);
  293. // Generating the Certificate
  294. var issuerKeyPair = subjectKeyPair;
  295. // selfsign certificate
  296. #pragma warning disable CS0618 // Type or member is obsolete
  297. var certificate = certificateGenerator.Generate(issuerPrivKey, random);
  298. #pragma warning restore CS0618 // Type or member is obsolete
  299. var key = new CertPrivateKey
  300. {
  301. KeyPair = subjectKeyPair
  302. };
  303. return (key, certificate);
  304. }
  305. public static X509Certificate2
  306. ToDotNetCert(CertPrivateKey key, BcCertificate certificate)
  307. {
  308. // merge into X509Certificate2
  309. // correcponding private key
  310. var info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(key.KeyPair.Private);
  311. var x509 = new X509Certificate2(
  312. certificate.GetEncoded());
  313. #pragma warning disable CS0618 // Type or member is obsolete
  314. var seq = (Asn1Sequence)Asn1Object.FromByteArray(info.PrivateKeyData.GetDerEncoded());
  315. #pragma warning restore CS0618 // Type or member is obsolete
  316. if (seq.Count != 9)
  317. throw new PemException("malformed sequence in RSA private key");
  318. #pragma warning disable CS0618 // Type or member is obsolete
  319. var rsa = new RsaPrivateKeyStructure(seq);
  320. #pragma warning restore CS0618 // Type or member is obsolete
  321. var rsaparams = new RsaPrivateCrtKeyParameters(
  322. rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent,
  323. rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2, rsa.Coefficient);
  324. x509.PrivateKey = DotNetUtilities.ToRSA(rsaparams);
  325. return x509;
  326. }
  327. public static BcCertificate ImportCertificate(EncodingFormat fmt, Stream source)
  328. {
  329. BcCertificate bcCert = null;
  330. if (fmt == EncodingFormat.DER)
  331. {
  332. var certParser = new X509CertificateParser();
  333. bcCert = certParser.ReadCertificate(source);
  334. }
  335. else if (fmt == EncodingFormat.PEM)
  336. {
  337. using (var tr = new StreamReader(source))
  338. {
  339. var pr = new PemReader(tr);
  340. bcCert = (BcCertificate)pr.ReadObject();
  341. }
  342. }
  343. else
  344. {
  345. throw new NotSupportedException("encoding format has not been implemented");
  346. }
  347. return bcCert;
  348. }
  349. public static void ExportCertificate(BcCertificate cert, EncodingFormat fmt, Stream target)
  350. {
  351. if (fmt == EncodingFormat.PEM)
  352. {
  353. using (var tw = new StringWriter())
  354. {
  355. var pw = new PemWriter(tw);
  356. pw.WriteObject(cert);
  357. var pemBytes = Encoding.UTF8.GetBytes(tw.GetStringBuilder().ToString());
  358. target.Write(pemBytes, 0, pemBytes.Length);
  359. }
  360. }
  361. else if (fmt == EncodingFormat.DER)
  362. {
  363. var der = cert.GetEncoded();
  364. target.Write(der, 0, der.Length);
  365. }
  366. else
  367. {
  368. throw new NotSupportedException("unsupported encoding format");
  369. }
  370. }
  371. public static void ExportArchive(CertPrivateKey pk, IEnumerable<BcCertificate> certs,
  372. ArchiveFormat fmt, Stream target, string password = null)
  373. {
  374. if (fmt == ArchiveFormat.PKCS12)
  375. {
  376. var bcCerts = certs.Select(x =>
  377. new X509CertificateEntry(x)).ToArray();
  378. var pfx = new Pkcs12Store();
  379. pfx.SetCertificateEntry(bcCerts[0].Certificate.SubjectDN.ToString(), bcCerts[0]);
  380. pfx.SetKeyEntry(bcCerts[0].Certificate.SubjectDN.ToString(),
  381. new AsymmetricKeyEntry(pk.KeyPair.Private), new[] { bcCerts[0] });
  382. for (var i = 1; i < bcCerts.Length; ++i)
  383. //pfx.SetCertificateEntry(bcCerts[i].Certificate.SubjectDN.ToString(),
  384. pfx.SetCertificateEntry(i.ToString(), bcCerts[i]);
  385. // It used to be pretty straight forward to export this...
  386. pfx.Save(target, password?.ToCharArray(), new SecureRandom());
  387. }
  388. else
  389. {
  390. throw new NotSupportedException("unsupported archive format");
  391. }
  392. }
  393. private static string ToPrivatePem(AsymmetricCipherKeyPair ackp)
  394. {
  395. string pem;
  396. using (var tw = new StringWriter())
  397. {
  398. var pw = new PemWriter(tw);
  399. pw.WriteObject(ackp.Private);
  400. pem = tw.GetStringBuilder().ToString();
  401. tw.GetStringBuilder().Clear();
  402. }
  403. return pem;
  404. }
  405. }
  406. }