CertHelper.cs 20 KB

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