1. はじめに
やってみた自分用メモ。「自己署名証明書」をなんとなく理解したと思っているので、次に「他人に証明してもらう証明書」を勉強してみました。
ちなみに前回はこちら。
また、以下の記事に感謝します。
2. サーバー証明書になるまでの流れ
今まではサーバーが「自己署名証明書」 を作っていました。「自分が対象」で「自分が承認」した結果「署名した」のだから「自己署名証明書」と言います。
その代わり、認証局(CA)に承認してもらって「証明書(CRT)」を作ってもらう、というのがゴールになります。「(認証局の承認による)証明書」になるわけですね。
・それには認証局(CA)に対して「承認してください」というお願いを出す必要があります。それが「証明書署名要求(CSR)」になります。
・「証明書署名要求(CSR)」を作るには自分(サーバーの)署名が必要になります。
いっぱい「署名」「証明書」という言葉が出てきて混乱しますね!
順番にまとめると、
- サーバーは証明書署名要求(CSR)を作ります。その時に署名します
- サーバーは証明書署名要求(CSR)を認証局に送ります
- 認証局(CA)はCSRに署名して、証明書(CRT)にします
- 認証局(CA)はサーバーに証明書(CRT)を送ります
です。
でも、認証局(CA)は基本的に他人(外部)です。この流れを体験したい、とか理解したいと思いますよね? 私は思いました。では自己認証局(CA)を作りましょう。
「自己承認によるサーバー証明書(自己署名証明書)」から「自己認証局(CA)の承認によるサーバー証明書」へと、一段深くなります。この場合も結局「承認局(CA)自身も自己承認(オレオレ承認局)」なので信頼性は皆無ですが、「流れを理解する」のが目的です。
サーバー、自己認証局という登場人物が揃っているので、クラスにします。
3. コード
ChatGPTの出力が元になっています。利用される方は自己責任で。
3-1. サーバーのコード
サーバーがやることは
- 秘密鍵、公開鍵を作る
- 秘密鍵で署名した証明書署名要求(CSR) を作る
証明書署名要求(CSR)には当然サーバーが作った公開鍵も含まれています。
コードには「pfx形式で保存する」もありますが、これは後で使いやすいようにするためです。
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)コード
-
自己認証局自身も署名するので 秘密鍵、公開鍵を持ちます
-
Createにはサーバーの自己証明書と非常に似たコードが並んでいます。違いと言えば「自分がCAであること」を示すパラメータが埋め込まれているくらいでしょうか。自己認証局(CA)の公開鍵が含まれていることにも留意してください
Issuer = Subject にして 自己認証局にしています。
-
署名の段階ではサーバーの証明書署名要求(CSR)が正しいかどうかを検証するところから始まります
-
サーバーの 証明書署名要求(CSR) に書いてあったパラメータ(CNやSAN)を読み込み、コピーしています。この辺りしつこいようですが、あくまでも流れを理解するための例ということでご容赦ください
-
最後に自己認証局(CA)の秘密鍵で署名しています
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からコントロールして終了です。
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. 証明書チェーンとは
次図のように認証局が連なります。日本語では「数珠繋ぎ」。チェーンです。
チェーンが長くなると、証明書の中には各認証局の公開鍵と署名が積み重なっていきます。結果、証明書のサイズが大きくなっていきます。
以下の記事に感謝します。