[C#] 向 CA Server 註冊 WebServer 憑證範本

為了註冊 CA 憑證動作更簡化,於是花了三四天研究怎麼用 C# 控制註冊流程,過程真的挺累,結果挺爽的...

本文連結

上一篇講到使用 MMC 申請 CA 憑證

https://dotblogs.com.tw/yc421206/2019/05/24/mmc_request_web_certificate_from_ca_server_can_trusted_by_chrome_browser#Web Server 通過 MMC 請求憑證
 

要能使用 MMC 申請 CA 憑證,要先加入網域,但我們透過 C# 就能打破這個限制

開發環境

  • VS 2019
  • CertEroll 1.0 Type Library
  • CertCli1.0 Type Library


請求憑證步驟

  1. 建立憑證要求
  2. 送出請求
  3. 註冊憑證

建立憑證請求

IX509PrivateKey:定義私鑰

CX509CertificateRequestPkcs10:憑證類型

CX509ExtensionAlternativeNames:SAN,能被 Chrome 信任的關鍵欄位,它在 GetSAN 方法,根據 CN 欄位自動幫你建立

CX500DistinguishedName:定義 Subject

public string CreateRequest(string cn, string ou, string o, string l, string s, string c,
                            string oid,
                            int    keyLength)
{
    var csp = new CCspInformations();
    csp.AddAvailableCsps();
 
    var privateKey =
        (IX509PrivateKey) Activator.CreateInstance(Type.GetTypeFromProgID("X509Enrollment.CX509PrivateKey"));
    privateKey.Length          = keyLength;
    privateKey.KeySpec         = X509KeySpec.XCN_AT_SIGNATURE;
    privateKey.KeyUsage        = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES;
    privateKey.MachineContext  = false;
    privateKey.ExportPolicy    = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG;
    privateKey.CspInformations = csp;
    privateKey.Create();
 
    var pkcs10 =
        (CX509CertificateRequestPkcs10)
        Activator.CreateInstance(Type.GetTypeFromProgID("X509Enrollment.CX509CertificateRequestPkcs10"));
 
    pkcs10.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextUser,
                                    privateKey,
                                    string.Empty);
 
    var extensionKeyUsage =
        (CX509ExtensionKeyUsage)
        Activator.CreateInstance(Type.GetTypeFromProgID("X509Enrollment.CX509ExtensionKeyUsage"));
    extensionKeyUsage.InitializeEncode(X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE |
                                       X509KeyUsageFlags.XCN_CERT_NON_REPUDIATION_KEY_USAGE   |
                                       X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE  |
                                       X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE);
    pkcs10.X509Extensions.Add((CX509Extension) extensionKeyUsage);
 
    var objectId  = new CObjectId();
    var objectIds = new CObjectIds();
 
    var extensionEnhancedKeyUsage =
        (CX509ExtensionEnhancedKeyUsage)
        Activator.CreateInstance(Type.GetTypeFromProgID("X509Enrollment.CX509ExtensionEnhancedKeyUsage"));
 
    objectId.InitializeFromValue(oid);
    objectIds.Add(objectId);
    extensionEnhancedKeyUsage.InitializeEncode(objectIds);
    pkcs10.X509Extensions.Add((CX509Extension) extensionEnhancedKeyUsage);
 
    var san = GetSAN(cn);
    pkcs10.X509Extensions.Add((CX509Extension) san);
 
    var distinguishedName =
        (CX500DistinguishedName)
        Activator.CreateInstance(Type.GetTypeFromProgID("X509Enrollment.CX500DistinguishedName"));
    cn = ConvertCn(cn);
 
    var subjectName = $"{cn},OU = {ou},O = {o} ,L = {l},S = {s},C = {c}";
    distinguishedName.Encode(subjectName);
    pkcs10.Subject = distinguishedName;
 
    var enroll =
        (CX509Enrollment) Activator.CreateInstance(Type.GetTypeFromProgID("X509Enrollment.CX509Enrollment"));
    enroll.InitializeFromRequest(pkcs10);
    var request = enroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64HEADER);
 
    return request;
}

 

這個步驟所得到的內容就能貼到 CA Server 的 Web 網頁手動要求憑證,如下圖


 

https://dotblogs.com.tw/yc421206/2013/07/26/112543#s2
 

下面的步驟則是透過 C# 完成憑證要求

 

送出請求

選擇向 CA Server 要求 WebServer 範本憑證

CA Server 的 URL 格式為 主機位置\CA名稱,比如 ad.lab.local\lab-ca

public string SendRequest(string createRequest, string caServer,
                          string templateName,
                          string additionalAttributes = "")
{
    var attributes = string.Format("CertificateTemplate: {0}", templateName);
 
    if (!string.IsNullOrEmpty(additionalAttributes))
    {
        attributes += "\n" + additionalAttributes;
    }
 
    var certRequest = new CCertRequest();
    var requestResult =
        (RequestDisposition) certRequest.Submit((int) EncodingType.XCN_CRYPT_STRING_BASE64HEADER,
                                                createRequest,
                                                attributes,
                                                caServer);
    string cert = null;
    if (requestResult == RequestDisposition.CR_DISP_ISSUED)
    {
        cert = certRequest.GetCertificate((int) EncodingType.XCN_CRYPT_STRING_BASE64REQUESTHEADER);
    }
 
    return cert;
}

 

註冊憑證

調用 enroll.InstallResponse 後,就會把憑證安裝在「目前使用者\個人」,這個位置不會被 IIS 認可,所以我要把它改安裝在「本機電腦\個人」

enroll.CreatePFX 產生 Base64 的憑證內容

public void InstallAndDownload(string certText, string password, string friendlyName)
{
    var enroll = new CX509Enrollment();
    enroll.Initialize(X509CertificateEnrollmentContext.ContextUser);
    enroll.CertificateFriendlyName = friendlyName;
    enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedRoot,
                           certText,
                           EncodingType.XCN_CRYPT_STRING_BASE64REQUESTHEADER,
                           password
                          );
    var dir = Directory.GetParent(Assembly.GetExecutingAssembly().Location).ToString();
    var pfx = enroll.CreatePFX(password, PFXExportOptions.PFXExportChainWithRoot);
 
    var fileName = "cert.pfx";
    var filePath = $@"{dir}\{fileName}";
    Download(filePath, pfx);
    Install(filePath, password);
}

 

我要用 X509Certificate2 把 Base64 憑證安裝到「本機電腦\個人」,我在實作的過程當中一直出現 Encode 的例外訊息;

後來改成,將 Base64 轉成陣列後再存檔,才解決這個問題

private static void Download(string filePath, string content)
{
    using (var fs = new FileStream(filePath, FileMode.Create))
    {
        var base64String = Convert.FromBase64String(content);
        fs.Write(base64String, 0, base64String.Length);
    }
}

 

private static void Install(string filePath, string password)
{
    var cert  = new X509Certificate2(filePath, password, X509KeyStorageFlags.PersistKeySet);
    var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
    store.Open(OpenFlags.ReadWrite);
    store.Add(cert);
}

 

這個動作需要管理員權限

專案位置

https://github.com/yaochangyu/sample.dotblog/tree/master/Cert/Lab.CertFromCA
 

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo