1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自己認証局を作ろう(証明書チェーンを理解したい)

Last updated at Posted at 2025-09-04

1. はじめに

やってみた自分用メモ。「自己署名証明書」をなんとなく理解したと思っているので、次に「他人に証明してもらう証明書」を勉強してみました。

ちなみに前回はこちら。

また、以下の記事に感謝します。

2. サーバー証明書になるまでの流れ

image.png

 今まではサーバーが「自己署名証明書」 を作っていました。「自分が対象」で「自分が承認」した結果「署名した」のだから「自己署名証明書」と言います。

 その代わり、認証局(CA)に承認してもらって「証明書(CRT)」を作ってもらう、というのがゴールになります。「(認証局の承認による)証明書」になるわけですね。

・それには認証局(CA)に対して「承認してください」というお願いを出す必要があります。それが「証明書署名要求(CSR)」になります。

・「証明書署名要求(CSR)」を作るには自分(サーバーの)署名が必要になります。

いっぱい「署名」「証明書」という言葉が出てきて混乱しますね!

順番にまとめると、

  1. サーバーは証明書署名要求(CSR)を作ります。その時に署名します
  2. サーバーは証明書署名要求(CSR)を認証局に送ります
  3. 認証局(CA)はCSRに署名して、証明書(CRT)にします
  4. 認証局(CA)はサーバーに証明書(CRT)を送ります

です。

でも、認証局(CA)は基本的に他人(外部)です。この流れを体験したい、とか理解したいと思いますよね? 私は思いました。では自己認証局(CA)を作りましょう。

「自己承認によるサーバー証明書(自己署名証明書)」から「自己認証局(CA)の承認によるサーバー証明書」へと、一段深くなります。この場合も結局「承認局(CA)自身も自己承認(オレオレ承認局)」なので信頼性は皆無ですが、「流れを理解する」のが目的です。

サーバー、自己認証局という登場人物が揃っているので、クラスにします。

3. コード

ChatGPTの出力が元になっています。利用される方は自己責任で。

3-1. サーバーのコード

サーバーがやることは

  1. 秘密鍵、公開鍵を作る
  2. 秘密鍵で署名した証明書署名要求(CSR) を作る

証明書署名要求(CSR)には当然サーバーが作った公開鍵も含まれています。

コードには「pfx形式で保存する」もありますが、これは後で使いやすいようにするためです。

server.cs
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;

using System;
using System.Collections.Generic;
using System.IO;

public class ServerCertificate
{
    public AsymmetricCipherKeyPair KeyPair { get; private set; }

    public void GenerateKey(string keyPath)
    {
        var keyGen = new RsaKeyPairGenerator();
        keyGen.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
        KeyPair = keyGen.GenerateKeyPair();

        using (var sw = new StreamWriter(keyPath))
        {
            var pemWriter = new PemWriter(sw);
            pemWriter.WriteObject(KeyPair.Private);
        }
    }

    // CSR を作成(サーバーの責務)
    public void GenerateCSRWithSAN(string csrPath, string subjectName, string[] dnsNames)
    {
        var san = new GeneralNames(Array.ConvertAll(dnsNames, dn => new GeneralName(GeneralName.DnsName, dn)));
        var extensions = new X509Extensions(
            new Dictionary<DerObjectIdentifier, X509Extension>
            {
                { X509Extensions.SubjectAlternativeName, new X509Extension(false, new DerOctetString(san)) }
            });

        var attr = new AttributePkcs(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest, new DerSet(extensions));
        var csr = new Pkcs10CertificationRequest(
            "SHA256WITHRSA",
            new X509Name(subjectName),
            KeyPair.Public,
            new DerSet(attr),
            KeyPair.Private
        );

        using (var sw = new StreamWriter(csrPath))
        {
            var pemWriter = new PemWriter(sw);
            pemWriter.WriteObject(csr);
        }
    }

    // PFX 作成
    public void CreatePfx(string keyFile, string certFile, string pfxFile, string password)
    {
        AsymmetricKeyParameter privateKey;
        using (var sr = new StreamReader(keyFile))
        {
            var obj = new PemReader(sr).ReadObject();
            privateKey = obj is AsymmetricCipherKeyPair kp ? kp.Private : (AsymmetricKeyParameter)obj;
        }

        X509Certificate cert;
        using (var sr = new StreamReader(certFile))
            cert = (X509Certificate)new PemReader(sr).ReadObject();

        var store = new Pkcs12StoreBuilder().Build();
        var certEntry = new X509CertificateEntry(cert);
        string friendlyName = cert.SubjectDN.ToString();

        store.SetCertificateEntry(friendlyName, certEntry);
        store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(privateKey), new[] { certEntry });

        using (var fs = new FileStream(pfxFile, FileMode.Create, FileAccess.Write))
            store.Save(fs, password.ToCharArray(), new SecureRandom());
    }
}

3-2. 自己認証局の(CA)コード

  1. 自己認証局自身も署名するので 秘密鍵、公開鍵を持ちます
     

  2. Createにはサーバーの自己証明書と非常に似たコードが並んでいます。違いと言えば「自分がCAであること」を示すパラメータが埋め込まれているくらいでしょうか。自己認証局(CA)の公開鍵が含まれていることにも留意してください

     Issuer = Subject にして 自己認証局にしています。
     

  3. 署名の段階ではサーバーの証明書署名要求(CSR)が正しいかどうかを検証するところから始まります
     

  4. サーバーの 証明書署名要求(CSR) に書いてあったパラメータ(CNやSAN)を読み込み、コピーしています。この辺りしつこいようですが、あくまでも流れを理解するための例ということでご容赦ください
     

  5. 最後に自己認証局(CA)の秘密鍵で署名しています

CertificateAuthority.cs
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;

using System;
using System.IO;

public class CertificateAuthority
{
    public AsymmetricKeyParameter PrivateKey { get; private set; }
    public X509Certificate Certificate { get; private set; }

    public void Create(string caKeyPath, string caCertPath)
    {
    	string strCA="CN=MyCA";
    	
        var keyGen = new RsaKeyPairGenerator();
        keyGen.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
        var keyPair = keyGen.GenerateKeyPair();
        PrivateKey = keyPair.Private;

        var certGen = new X509V3CertificateGenerator();
        certGen.SetSerialNumber(BigInteger.ProbablePrime(120, new Random()));
        // Issuer = Subject(自己認証局)
        certGen.SetIssuerDN(new X509Name(strCA));
        certGen.SetSubjectDN(new X509Name(strCA));
        certGen.SetNotBefore(DateTime.UtcNow.AddMinutes(-5));
        certGen.SetNotAfter(DateTime.UtcNow.AddYears(10));
        // CAの公開鍵を埋め込む
        certGen.SetPublicKey(keyPair.Public);

        // CAであることを示す
        certGen.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(true));
        // 証明書発行と CRL 署名用
        certGen.AddExtension(X509Extensions.KeyUsage, true,
            new KeyUsage(KeyUsage.KeyCertSign | KeyUsage.CrlSign));

		// CAの秘密鍵で署名
        var signatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", keyPair.Private);
        Certificate = certGen.Generate(signatureFactory);

        SaveAsPem(caCertPath, Certificate);
        SaveAsPem(caKeyPath, PrivateKey);
    }

    // CSR を受け取って署名
    public X509Certificate SignCSR(Pkcs10CertificationRequest csr, int validYears = 1)
    {
        if (!csr.Verify())
        {
            throw new InvalidOperationException("CSR の検証に失敗しました。");
        }

        // サーバーの公開鍵
        var serverPublicKey = csr.GetPublicKey();
        var certGen = new X509V3CertificateGenerator();

        certGen.SetSerialNumber(BigInteger.ProbablePrime(120, new Random()));
        certGen.SetIssuerDN(Certificate.SubjectDN);
        // CSRに書いてあったSubject(CNなど)をコピー。そのまま信用しないとか、加工するとかいろいろ。
        certGen.SetSubjectDN(csr.GetCertificationRequestInfo().Subject);
        certGen.SetNotBefore(DateTime.UtcNow.AddMinutes(-5));
        certGen.SetNotAfter(DateTime.UtcNow.AddYears(validYears));
        // サーバーの公開鍵
        certGen.SetPublicKey(serverPublicKey);

		// サーバー認証用
        // KeyUsage / ExtendedKeyUsage
        certGen.AddExtension(X509Extensions.KeyUsage, true,
            new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment));
        certGen.AddExtension(X509Extensions.ExtendedKeyUsage, false, new ExtendedKeyUsage(KeyPurposeID.id_kp_serverAuth));

        // CSR から SAN を取得してそのままコピー
        var csrInfo = csr.GetCertificationRequestInfo();
        X509Extensions requestedExtensions = null;
        if (csrInfo.Attributes != null)
        {
            foreach (Asn1Encodable ae in csrInfo.Attributes)
            {
                var attr = AttributePkcs.GetInstance(ae);
                if (attr.AttrType.Equals(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest))
                {
                    requestedExtensions = X509Extensions.GetInstance(attr.AttrValues[0]);
                    break;
                }
            }
        }

        if (requestedExtensions != null)
        {
            var sanExt = requestedExtensions.GetExtension(X509Extensions.SubjectAlternativeName);
            if (sanExt != null)
            {
                certGen.AddExtension(X509Extensions.SubjectAlternativeName, false, Asn1Object.FromByteArray(sanExt.Value.GetOctets()));
            }
        }
        // CAの秘密鍵で署名して返す
        return certGen.Generate(new Asn1SignatureFactory("SHA256WITHRSA", PrivateKey));
    }

    private static void SaveAsPem(string fileName, object obj)
    {
        using (var writer = new StreamWriter(fileName))
        {
            var pemWriter = new PemWriter(writer);
            pemWriter.WriteObject(obj);
        }
    }
}

3-3. Main

これらをMainからコントロールして終了です。

program.cs
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;

using System;
using System.IO;

internal class Program
{
    static void Main(string[] args)
    {
        // ===== CA作成 =====
        var ca = new CertificateAuthority();
        ca.Create("ca.key", "ca.crt");

        // ===== サーバー鍵生成 =====
        var server = new ServerCertificate();
        server.GenerateKey("server.key");

        // ===== SAN付き CSR作成 =====
        server.GenerateCSRWithSAN("server.csr", "CN=localhost", new string[] { "localhost", "example.com" });

        // ===== CAによるサーバー証明書署名 =====
        Pkcs10CertificationRequest csr;
        using (var sr = new StreamReader("server.csr"))
        {
            csr = (Pkcs10CertificationRequest)new PemReader(sr).ReadObject();
        }

        var signedCert = ca.SignCSR(csr);

        // サーバー証明書を PEM で保存
        using (var sw = new StreamWriter("server.crt"))
        {
            var pemWriter = new PemWriter(sw);
            pemWriter.WriteObject(signedCert);
        }

        // ===== PFX作成 =====
        server.CreatePfx("server.key", "server.crt", "server.pfx", "p@ssw0rd");

        Console.WriteLine("サーバー証明書と PFX を作成しました。");
    }
}

4. 証明書チェーンとは

次図のように認証局が連なります。日本語では「数珠繋ぎ」。チェーンです。

チェーンが長くなると、証明書の中には各認証局の公開鍵と署名が積み重なっていきます。結果、証明書のサイズが大きくなっていきます。

以下の記事に感謝します。

image.png

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?