인증서 (Certification - .cer) 만들기




오늘은 C# 을 이용해서 인증서를 만드는 방법에 대해서 설명드리도록 하겠습니다.

아마도 다들 인증서를 많이 들어보셨을거라고 생각합니다.

.pfx .cer 같은 확장자를 가지고 있는 파일이지요.


각 파일의 차이점은 직접 찾아보시면서 공부하시는게 좋을 것 같습니다.

primary key 를 포함하느냐 마느냐부터 시작해서 여러가지 차이가 있습니다 :)






ssh 나 ssl 등 보안 프로토콜/통신에서 많이 쓰이지요.

기본적으로 윈도우의 makecert.exe 를 통해서 만들 수 있긴 합니다.

(visual studio 설치시 함께 설치됩니다)

하지만 많은 옵션을 제공함에도 불구하고, 빠진 것들이 많습니다.


저의 경우 SAN(Subject Alternative Name)의 값이 포함된 cer 파일을 만들었어야 했습니다.

하지만 makecert.exe 에서는 이를 지원하지 않습니다. orz


하여, 여러가지 방법으로 인증서를 만들기 위해서 시도해 보았습니다. :)

BUT!!!

이렇게 만든 인증서들은 내부적으로 쓰는 알고리즘이 달라서 그런지, 추후 'netsh.exe http delete sslcert' 를 통해서

포트 바인딩을 시킬 수가 없었습니다.


자꾸만,,, 윈도우에서 해당 인증서가 올바르지 않다거나, 사용할수 없다면서...

도무지 쓸 수가 없더군요.

물론 인증서는 유효하고, 이상이 없습니다.

다만 윈도우 netsh.exe 에서 사용하는 형식과 다르다는 겁니다.

요컨데 makecert.exe 를 통해 만든것과 제가 만든 것이 무언가 다르다는 것이지요.


인증서를 조금 보신분들은 아시겠지만, 수많은 암호화 알고리즘을 선택해서 사용할 수 있으며,

primary key 를 포함할꺼냐 말꺼냐 하는 등의 많은 선택사항들이 있습니다.

이를 일치시키는것은(netsh 에서 쓸수 있게 하기 위해서..), 여간 귀찮은 일이 아닙니다.



이제 그 방법에 대해서 설명드리고자 합니다.

먼저 BouncyCastle.Crypto 를 다운로드 받습니다.

Visual Studio 의 NuGet 에서 'Bouncy Castle' 로 검색하면 됩니다.




이후 이를 사용해서 인증서를 만들면 되는데, 그냥 만들어버리면 netsh.exe 에서 사용할 수가 없습니다 =_=;

고로 수정을 해야 했습니다.


코드는 길지 않으니, 설명을 대체하도록 하겠습니다 :)


    [Certificate.cs]


using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Pkix;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.X509.Extension;
using Org.BouncyCastle.X509.Store;
using Org.BouncyCastle.Asn1;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;

namespace Bitsforest.Net.Security
{
    public class Certificate
    {
        public static X509Certificate2 CreateRootCA(string domainName, DateTime fromDate, DateTime toDate, out CertificateKeyPair keyPair, int keySize = 1024)
        {
            // First you need to generate a key pair. We are using "RSA" public-key cryptography algorithm and a key size of 1024.
            RsaKeyPairGenerator keyPairGenerator = new RsaKeyPairGenerator();
            keyPairGenerator.Init(new Org.BouncyCastle.Crypto.KeyGenerationParameters(new SecureRandom(), keySize));
            AsymmetricCipherKeyPair newKeyPair = keyPairGenerator.GenerateKeyPair();

            // Then instantiate an X.509 cert. generator.
            X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();

            // Now start creating the certificate. Serial number, issuer, validity period and Subject are set here.
            certGenerator.SetSerialNumber(BigInteger.ProbablePrime(120, new Random()));
            certGenerator.SetIssuerDN(new X509Name(domainName));
            certGenerator.SetNotBefore(fromDate); // DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0))
            certGenerator.SetNotAfter(toDate); // 
            certGenerator.SetSubjectDN(new X509Name(domainName));

            // Then set the public key of the key pair and the signing algorithm to the cert generator.
            certGenerator.SetPublicKey(newKeyPair.Public);
            certGenerator.SetSignatureAlgorithm("SHA1WithRSAEncryption");

            // Now you can generate the certificate.
            X509Certificate certificate = certGenerator.Generate(newKeyPair.Private, new SecureRandom());
            certificate.Verify(newKeyPair.Public);

            //And store it in a file.
            //System.Security.Cryptography.X509Certificates.X509Certificate certificate = DotNetUtilities.ToX509Certificate(PKCertificate);
            keyPair = new CertificateKeyPair(newKeyPair);
            return new X509Certificate2(DotNetUtilities.ToX509Certificate(certificate));
        }


        public static X509Certificate2 CreateCertificate(X509Certificate2 caCert, CertificateKeyPair caKeyPair, string domainName, DateTime fromDate, DateTime toDate, IEnumerable<string> subjectAlternativeNames, int keySize = 1024)
        {
            new RsaKeyPairGenerator().Init(new KeyGenerationParameters(new SecureRandom(), keySize));
            RsaKeyPairGenerator keyPairGenerator = new RsaKeyPairGenerator();
            keyPairGenerator.Init(new KeyGenerationParameters(new SecureRandom(), keySize));
            AsymmetricCipherKeyPair keyPair = keyPairGenerator.GenerateKeyPair();

            X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
            certGenerator.SetSerialNumber(BigInteger.ProbablePrime(120, new Random()));
            certGenerator.SetSubjectDN(new X509Name(domainName));
            certGenerator.SetIssuerDN(new X509Name(caCert.SubjectName.Name));
            certGenerator.SetNotBefore(fromDate);
            certGenerator.SetNotAfter(toDate);
            certGenerator.SetPublicKey(keyPair.Public);
            certGenerator.SetSignatureAlgorithm("SHA1WithRSAEncryption");

            // subject alternative name
            var sans = subjectAlternativeNames.Select(n => (Asn1Encodable)new GeneralName(GeneralName.DnsName, n)).ToArray();
            //var sans = new Asn1Encodable[] { new GeneralName(GeneralName.DnsName, "server"), new GeneralName(GeneralName.DnsName, "server.mydomain.com") };
            var subjectAlternativeNamesExtension = new DerSequence(sans);
            certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false, subjectAlternativeNamesExtension);

            certGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure(DotNetUtilities.FromX509Certificate(caCert)));
            certGenerator.AddExtension(X509Extensions.SubjectKeyIdentifier, false, new SubjectKeyIdentifierStructure(keyPair.Public));


            X509Certificate cert = certGenerator.Generate(caKeyPair.PrivateKey);
            cert.Verify(caKeyPair.PublicKey);

            var dotNetPrivateKey = ToDotNetKey((RsaPrivateCrtKeyParameters)keyPair.Private);
            X509Certificate2 certResult = new X509Certificate2(DotNetUtilities.ToX509Certificate(cert));
            certResult.PrivateKey = dotNetPrivateKey;
            return certResult;
        }


        public static void SaveToCert(X509Certificate2 certificate, string filePath)
        {
            byte[] data = certificate.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Cert);
            File.WriteAllBytes(filePath, data);
        }


        public static void SaveToPfx(X509Certificate2 certificate, string filePath, string password = null)
        {
            byte[] data = certificate.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pfx, (string)password);
            File.WriteAllBytes(filePath, data);
        }



        private static AsymmetricAlgorithm ToDotNetKey(RsaPrivateCrtKeyParameters privateKey)
        {
            var cspParams = new CspParameters
            {
                KeyContainerName = Guid.NewGuid().ToString(),
                KeyNumber = (int)KeyNumber.Exchange,
                Flags = CspProviderFlags.UseMachineKeyStore
            };

            var rsaProvider = new RSACryptoServiceProvider(cspParams);
            var parameters = new RSAParameters
            {
                Modulus = privateKey.Modulus.ToByteArrayUnsigned(),
                P = privateKey.P.ToByteArrayUnsigned(),
                Q = privateKey.Q.ToByteArrayUnsigned(),
                DP = privateKey.DP.ToByteArrayUnsigned(),
                DQ = privateKey.DQ.ToByteArrayUnsigned(),
                InverseQ = privateKey.QInv.ToByteArrayUnsigned(),
                D = privateKey.Exponent.ToByteArrayUnsigned(),
                Exponent = privateKey.PublicExponent.ToByteArrayUnsigned()
            };

            rsaProvider.ImportParameters(parameters);
            return rsaProvider;
        }



        public static void Store(X509Certificate2 cert, StoreName name, StoreLocation location)
        {
            X509Store storeMy = new X509Store(name, location);
            storeMy.Open(OpenFlags.ReadWrite);
            storeMy.Add(cert);
            storeMy.Close();
        }

    }
}






    [CertificateKeyPair.cs]


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Org.BouncyCastle.Crypto;

namespace Bitsforest.Net.Security
{
    public class CertificateKeyPair
    {
        private AsymmetricCipherKeyPair _keyPair;

        public AsymmetricKeyParameter PrivateKey
        {
            get { return this._keyPair.Private; }
        }

        public AsymmetricKeyParameter PublicKey
        {
            get { return this._keyPair.Public; }
        }


        internal CertificateKeyPair(AsymmetricCipherKeyPair keypair)
        {
            this._keyPair = keypair;
        }
    }
}





핵심 코드는 위 두가지입니다.

추가로 netsh.exe 를 통해서 포트바인딩 하는 클래스입니다.



    [Network.cs]


using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Pkix;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.X509.Extension;
using Org.BouncyCastle.X509.Store;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
using Bitsforest.Net.Security;

namespace Bitsforest.Net.Security
{
    public class Network
    {
        public static bool BindToPort(X509Certificate2 cert, int port, string appId)
        {
            string command = string.Format("http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}", port, cert.Thumbprint, appId);
            System.Diagnostics.Process process = new System.Diagnostics.Process();
            process.StartInfo.FileName = "netsh.exe";
            process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
            process.StartInfo.Arguments = command;
            process.Start();
            process.WaitForExit();
            return process.ExitCode == 0;
        }


        public static bool DeletePortBinding(int port)
        {
            string command = string.Format("http delete sslcert ipport=0.0.0.0:{0}", port);
            System.Diagnostics.Process process = new System.Diagnostics.Process();
            process.StartInfo.FileName = "netsh.exe";
            process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
            process.StartInfo.Arguments = command;
            process.Start();
            process.WaitForExit();
            return process.ExitCode == 0;
        }


        public static bool FlushDns()
        {
            string command = string.Format("/flushdns");
            System.Diagnostics.Process process = new System.Diagnostics.Process();
            process.StartInfo.FileName = "ipconfig.exe";
            process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
            process.StartInfo.Arguments = command;
            process.Start();
            process.WaitForExit();
            return process.ExitCode == 0;
        }
    }
}






매우 간단하죠? ^-^

사용방법은 아래와 같습니다.


    [TestCode]


using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Bitsforest.Net.Security;

namespace CertTest
{
    class Program
    {
        static void Main(string[] args)
        {
            bool result = Network.DeletePortBinding(443);
            Console.WriteLine("Delete Binding : " + result);

            CertificateKeyPair keyPair;
            X509Certificate2 caCert = Certificate.CreateRootCA("CN=BitsforestRootCA", DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)), DateTime.Now.Add(new TimeSpan(7, 0, 0, 0)), out keyPair);

            List<string> san = new List<string>();
            san.Add("github.com");
            X509Certificate2 chainCert = Certificate.CreateCertificate(caCert, keyPair, "CN=Bitsforest", DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)), DateTime.Now.Add(new TimeSpan(7, 0, 0, 0)), san);

            Certificate.Store(caCert, StoreName.AuthRoot, StoreLocation.LocalMachine);
            Certificate.Store(chainCert, StoreName.My, StoreLocation.LocalMachine);

            result = Network.BindToPort(chainCert, 443, "b4bc4668-8add-4d68-8989-4f5ae5cf63d5");
            Console.WriteLine("Binding : " + result);

            result = Network.FlushDns();
            Console.WriteLine("Flush Dns: " + result);
        }
    }
}





코드가 하는 내용은 root 인증서를 만들고, 그 인증서를 이용해서 자식 인증서를 만듭니다.

그리고 그렇게 만든 자식인증서를 가지고 포트바인딩을 하는 코드입니다.



그럼 포스팅을 마치도록 하겠습니다. :)

절대 귀찮아서 그런건 아니에요;;;