Certes

SSL証明書C#ACMELet's Encryptバリデーション.NET

ライブラリ

Certes

概要

CertesはACME(Automated Certificate Management Environment)プロトコルの.NET Core実装で、Let's Encryptを始めとする証明書機関からSSL証明書を自動取得・管理するライブラリです。証明書の有効性検証、ドメイン所有権の確認、証明書の自動更新機能を提供します。DNS-01およびHTTP-01チャレンジをサポートし、ワイルドカード証明書の取得も可能です。.NET Coreの非同期パターンを活用した効率的な証明書管理により、Webアプリケーションのセキュリティを強化します。

詳細

Certesは、ACMEプロトコルv2に準拠したクライアント実装で、Let's Encryptをはじめとする証明書機関との通信を自動化します。証明書の取得プロセスにおいて、ドメイン所有権の検証、証明書署名要求(CSR)の生成、証明書の取得と保存を一貫して処理します。バリデーション機能として、証明書の有効期限チェック、ドメイン名の検証、証明書チェーンの検証を提供します。90日間の証明書有効期限に対応した自動更新機能により、証明書の期限切れを防止できます。

主な特徴

  • ACME v2対応: Let's Encryptの最新プロトコルに完全対応
  • ワイルドカード証明書: DNS-01チャレンジによるワイルドカード証明書取得
  • 証明書検証: 有効期限、ドメイン名、証明書チェーンの包括的な検証
  • 自動更新: 証明書の自動更新とローテーション機能
  • 非同期処理: .NET Coreの非同期パターンを活用
  • 複数プロバイダー: Let's Encrypt以外のACME対応CAもサポート

メリット・デメリット

メリット

  • Let's Encryptとの完全な統合により無料SSL証明書を自動取得
  • ワイルドカード証明書の取得が可能
  • 証明書の有効期限管理と自動更新
  • .NET Coreエコシステムとの親和性
  • 包括的な証明書検証機能
  • 本番環境での豊富な運用実績

デメリット

  • ACME対応の証明書機関に限定される
  • DNS-01チャレンジには外部DNSプロバイダーとの連携が必要
  • 証明書の有効期限が90日と短い
  • 初期設定とドメイン検証の複雑さ
  • 高頻度な証明書更新によるシステム負荷
  • エラーハンドリングが複雑

参考ページ

書き方の例

インストールと基本セットアップ

# NuGetパッケージのインストール
dotnet add package Certes

# PackageManagerコンソールを使用する場合
Install-Package Certes

# .NET CLI を使用してプロジェクトを作成
dotnet new console -n CertesExample
cd CertesExample
dotnet add package Certes

基本的な証明書取得とバリデーション

using Certes;
using Certes.Acme;
using Certes.Acme.Resource;
using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    private static readonly string StagingDirectoryUri = WellKnownServers.LetsEncryptStagingV2;
    private static readonly string ProductionDirectoryUri = WellKnownServers.LetsEncryptV2;

    static async Task Main(string[] args)
    {
        try
        {
            await BasicCertificateExample();
            await CertificateValidationExample();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"エラー: {ex.Message}");
        }
    }

    // 基本的な証明書取得
    private static async Task BasicCertificateExample()
    {
        Console.WriteLine("=== 基本的な証明書取得 ===");
        
        // ACMEクライアントの作成(テスト環境を使用)
        var acme = new AcmeContext(StagingDirectoryUri);
        
        // アカウントの作成
        var account = await acme.NewAccount("[email protected]", true);
        Console.WriteLine($"アカウント作成成功: {account.Location}");
        
        // アカウントキーの保存(実際の実装では安全な場所に保存)
        var accountKey = acme.AccountKey.ToPem();
        await File.WriteAllTextAsync("account.key", accountKey);
        Console.WriteLine("アカウントキーを保存しました");
        
        // 証明書の注文作成
        var order = await acme.NewOrder(new[] { "test.example.com" });
        Console.WriteLine($"注文作成成功: {order.Location}");
        
        // 認証の取得
        var authz = (await order.Authorizations()).First();
        Console.WriteLine($"認証が必要なドメイン: {authz.Identifier}");
        
        // HTTPチャレンジの取得
        var httpChallenge = await authz.Http();
        if (httpChallenge != null)
        {
            Console.WriteLine($"HTTPチャレンジトークン: {httpChallenge.Token}");
            Console.WriteLine($"認証キー: {httpChallenge.KeyAuthz}");
            Console.WriteLine($"チャレンジURL: http://{authz.Identifier}/.well-known/acme-challenge/{httpChallenge.Token}");
            Console.WriteLine("上記URLでキー認証を公開してください");
        }
        
        // 実際の証明書取得は、チャレンジを完了した後に実行
        // await httpChallenge.Validate();
        // var privateKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
        // var cert = await order.Generate(new CsrInfo { CommonName = "test.example.com" }, privateKey);
    }

    // 証明書の検証
    private static async Task CertificateValidationExample()
    {
        Console.WriteLine("\n=== 証明書の検証 ===");
        
        // 保存されたアカウントキーの読み込み
        if (File.Exists("account.key"))
        {
            var accountKeyPem = await File.ReadAllTextAsync("account.key");
            var accountKey = KeyFactory.FromPem(accountKeyPem);
            
            var acme = new AcmeContext(StagingDirectoryUri, accountKey);
            
            // アカウント情報の取得
            var account = await acme.Account();
            Console.WriteLine($"アカウント状態: {account.Status}");
            Console.WriteLine($"連絡先: {string.Join(", ", account.Contact ?? new string[0])}");
            
            // アカウントが有効かどうかの検証
            if (account.Status == AccountStatus.Valid)
            {
                Console.WriteLine("✓ アカウントは有効です");
            }
            else
            {
                Console.WriteLine($"✗ アカウントの状態が無効です: {account.Status}");
            }
        }
        else
        {
            Console.WriteLine("アカウントキーが見つかりません");
        }
    }
}

高度な証明書管理とバリデーション

using Certes;
using Certes.Acme;
using Certes.Acme.Resource;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

// 証明書管理クラス
public class CertificateManager
{
    private readonly AcmeContext _acmeContext;
    private readonly string _accountKeyPath;
    private readonly string _certificateStorePath;
    private readonly bool _useProduction;

    public CertificateManager(string accountKeyPath, string certificateStorePath, bool useProduction = false)
    {
        _accountKeyPath = accountKeyPath;
        _certificateStorePath = certificateStorePath;
        _useProduction = useProduction;
        
        var directoryUri = useProduction ? WellKnownServers.LetsEncryptV2 : WellKnownServers.LetsEncryptStagingV2;
        _acmeContext = new AcmeContext(directoryUri);
    }

    // アカウントの初期化
    public async Task<bool> InitializeAccountAsync(string email)
    {
        try
        {
            IAccountContext account;
            
            if (File.Exists(_accountKeyPath))
            {
                // 既存のアカウントキーを読み込み
                var accountKeyPem = await File.ReadAllTextAsync(_accountKeyPath);
                var accountKey = KeyFactory.FromPem(accountKeyPem);
                _acmeContext.AccountKey = accountKey;
                
                account = await _acmeContext.Account();
                Console.WriteLine($"既存のアカウントを読み込みました: {account.Location}");
            }
            else
            {
                // 新しいアカウントを作成
                account = await _acmeContext.NewAccount(email, true);
                
                // アカウントキーを保存
                var accountKeyPem = _acmeContext.AccountKey.ToPem();
                await File.WriteAllTextAsync(_accountKeyPath, accountKeyPem);
                Console.WriteLine($"新しいアカウントを作成しました: {account.Location}");
            }
            
            return account.Resource.Status == AccountStatus.Valid;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"アカウント初期化エラー: {ex.Message}");
            return false;
        }
    }

    // 証明書の取得
    public async Task<CertificateResult> ObtainCertificateAsync(string[] domains, CertificateType certificateType = CertificateType.RSA)
    {
        try
        {
            // 証明書の注文を作成
            var order = await _acmeContext.NewOrder(domains);
            Console.WriteLine($"証明書注文を作成しました: {order.Location}");

            // 各ドメインの認証を処理
            var authorizations = await order.Authorizations();
            var challenges = new List<ChallengeInfo>();

            foreach (var authz in authorizations)
            {
                var domain = authz.Identifier;
                Console.WriteLine($"ドメイン {domain} の認証を開始します");

                // DNSチャレンジを優先(ワイルドカード証明書の場合)
                var dnsChallenge = await authz.Dns();
                if (dnsChallenge != null)
                {
                    var dnsTxt = _acmeContext.AccountKey.DnsTxt(dnsChallenge.Token);
                    challenges.Add(new ChallengeInfo
                    {
                        Domain = domain,
                        Type = ChallengeType.Dns01,
                        Token = dnsChallenge.Token,
                        KeyAuthorization = dnsTxt,
                        Challenge = dnsChallenge
                    });
                    
                    Console.WriteLine($"DNS TXTレコードを設定してください:");
                    Console.WriteLine($"  ホスト: _acme-challenge.{domain}");
                    Console.WriteLine($"  値: {dnsTxt}");
                }
                else
                {
                    // HTTPチャレンジを使用
                    var httpChallenge = await authz.Http();
                    if (httpChallenge != null)
                    {
                        challenges.Add(new ChallengeInfo
                        {
                            Domain = domain,
                            Type = ChallengeType.Http01,
                            Token = httpChallenge.Token,
                            KeyAuthorization = httpChallenge.KeyAuthz,
                            Challenge = httpChallenge
                        });
                        
                        Console.WriteLine($"HTTPチャレンジファイルを設置してください:");
                        Console.WriteLine($"  URL: http://{domain}/.well-known/acme-challenge/{httpChallenge.Token}");
                        Console.WriteLine($"  内容: {httpChallenge.KeyAuthz}");
                    }
                }
            }

            // ユーザーの確認を待つ
            Console.WriteLine("チャレンジの設定が完了したら、Enterキーを押してください...");
            Console.ReadLine();

            // チャレンジを検証
            foreach (var challengeInfo in challenges)
            {
                Console.WriteLine($"ドメイン {challengeInfo.Domain} のチャレンジを検証中...");
                
                var challenge = await challengeInfo.Challenge.Validate();
                
                // 検証結果を待つ
                while (challenge.Status == ChallengeStatus.Pending || challenge.Status == ChallengeStatus.Processing)
                {
                    await Task.Delay(2000);
                    challenge = await challengeInfo.Challenge.Resource();
                }
                
                if (challenge.Status == ChallengeStatus.Valid)
                {
                    Console.WriteLine($"✓ ドメイン {challengeInfo.Domain} の検証成功");
                }
                else
                {
                    Console.WriteLine($"✗ ドメイン {challengeInfo.Domain} の検証失敗: {challenge.Status}");
                    if (challenge.Error != null)
                    {
                        Console.WriteLine($"  エラー: {challenge.Error.Detail}");
                    }
                    
                    return new CertificateResult
                    {
                        Success = false,
                        ErrorMessage = $"ドメイン {challengeInfo.Domain} の検証に失敗しました"
                    };
                }
            }

            // 証明書の生成
            var privateKey = certificateType == CertificateType.RSA 
                ? KeyFactory.NewKey(KeyAlgorithm.RS256) 
                : KeyFactory.NewKey(KeyAlgorithm.ES256);
            
            var csrInfo = new CsrInfo
            {
                CommonName = domains.First(),
                CountryName = "JP",
                State = "Tokyo",
                Locality = "Tokyo",
                Organization = "Example Organization",
                OrganizationUnit = "IT Department"
            };

            var cert = await order.Generate(csrInfo, privateKey);
            
            // 証明書を保存
            var certificatePath = Path.Combine(_certificateStorePath, $"{domains.First()}.crt");
            var privateKeyPath = Path.Combine(_certificateStorePath, $"{domains.First()}.key");
            
            await File.WriteAllTextAsync(certificatePath, cert.ToPem());
            await File.WriteAllTextAsync(privateKeyPath, privateKey.ToPem());
            
            Console.WriteLine($"証明書を保存しました: {certificatePath}");
            Console.WriteLine($"秘密鍵を保存しました: {privateKeyPath}");

            return new CertificateResult
            {
                Success = true,
                CertificatePath = certificatePath,
                PrivateKeyPath = privateKeyPath,
                Certificate = cert,
                PrivateKey = privateKey
            };
        }
        catch (Exception ex)
        {
            Console.WriteLine($"証明書取得エラー: {ex.Message}");
            return new CertificateResult
            {
                Success = false,
                ErrorMessage = ex.Message
            };
        }
    }

    // 証明書の検証
    public async Task<CertificateValidationResult> ValidateCertificateAsync(string certificatePath)
    {
        try
        {
            if (!File.Exists(certificatePath))
            {
                return new CertificateValidationResult
                {
                    IsValid = false,
                    Errors = new[] { "証明書ファイルが見つかりません" }
                };
            }

            var certificatePem = await File.ReadAllTextAsync(certificatePath);
            var certificate = new X509Certificate2(System.Text.Encoding.UTF8.GetBytes(certificatePem));
            
            var validationResult = new CertificateValidationResult
            {
                IsValid = true,
                Certificate = certificate,
                Subject = certificate.Subject,
                Issuer = certificate.Issuer,
                NotBefore = certificate.NotBefore,
                NotAfter = certificate.NotAfter,
                SerialNumber = certificate.SerialNumber,
                Thumbprint = certificate.Thumbprint
            };

            var errors = new List<string>();

            // 有効期限の確認
            if (certificate.NotAfter < DateTime.Now)
            {
                errors.Add("証明書の有効期限が切れています");
                validationResult.IsValid = false;
            }
            else if (certificate.NotAfter < DateTime.Now.AddDays(30))
            {
                errors.Add("証明書の有効期限が30日以内に切れます");
                validationResult.HasWarnings = true;
            }

            // 発行日の確認
            if (certificate.NotBefore > DateTime.Now)
            {
                errors.Add("証明書はまだ有効になっていません");
                validationResult.IsValid = false;
            }

            // 証明書チェーンの検証
            var chain = new X509Chain();
            chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
            chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
            
            bool chainIsValid = chain.Build(certificate);
            if (!chainIsValid)
            {
                var chainErrors = chain.ChainStatus.Select(status => status.StatusInformation).ToArray();
                errors.AddRange(chainErrors);
                validationResult.IsValid = false;
            }

            validationResult.Errors = errors.ToArray();
            
            return validationResult;
        }
        catch (Exception ex)
        {
            return new CertificateValidationResult
            {
                IsValid = false,
                Errors = new[] { $"証明書の検証中にエラーが発生しました: {ex.Message}" }
            };
        }
    }

    // 証明書の更新が必要かどうかを確認
    public async Task<bool> NeedsRenewalAsync(string certificatePath, int daysBeforeExpiry = 30)
    {
        try
        {
            var validationResult = await ValidateCertificateAsync(certificatePath);
            
            if (!validationResult.IsValid)
            {
                return true; // 無効な証明書は更新が必要
            }

            if (validationResult.Certificate == null)
            {
                return true;
            }

            var expiryDate = validationResult.Certificate.NotAfter;
            var renewalDate = expiryDate.AddDays(-daysBeforeExpiry);
            
            return DateTime.Now >= renewalDate;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"証明書更新判定エラー: {ex.Message}");
            return true; // エラーが発生した場合は更新を試行
        }
    }
}

// 補助クラス
public class ChallengeInfo
{
    public string Domain { get; set; }
    public ChallengeType Type { get; set; }
    public string Token { get; set; }
    public string KeyAuthorization { get; set; }
    public IChallengeContext Challenge { get; set; }
}

public enum ChallengeType
{
    Http01,
    Dns01
}

public enum CertificateType
{
    RSA,
    ECDSA
}

public class CertificateResult
{
    public bool Success { get; set; }
    public string ErrorMessage { get; set; }
    public string CertificatePath { get; set; }
    public string PrivateKeyPath { get; set; }
    public CertificateChain Certificate { get; set; }
    public IKey PrivateKey { get; set; }
}

public class CertificateValidationResult
{
    public bool IsValid { get; set; }
    public bool HasWarnings { get; set; }
    public string[] Errors { get; set; }
    public X509Certificate2 Certificate { get; set; }
    public string Subject { get; set; }
    public string Issuer { get; set; }
    public DateTime NotBefore { get; set; }
    public DateTime NotAfter { get; set; }
    public string SerialNumber { get; set; }
    public string Thumbprint { get; set; }
}

// 使用例
class Program
{
    static async Task Main(string[] args)
    {
        var manager = new CertificateManager(
            accountKeyPath: "account.key",
            certificateStorePath: "./certificates",
            useProduction: false // テスト環境を使用
        );

        // アカウントの初期化
        bool accountInitialized = await manager.InitializeAccountAsync("[email protected]");
        if (!accountInitialized)
        {
            Console.WriteLine("アカウントの初期化に失敗しました");
            return;
        }

        // 証明書の取得
        var domains = new[] { "test.example.com", "www.test.example.com" };
        var result = await manager.ObtainCertificateAsync(domains);
        
        if (result.Success)
        {
            Console.WriteLine("証明書の取得に成功しました");
            
            // 証明書の検証
            var validationResult = await manager.ValidateCertificateAsync(result.CertificatePath);
            
            if (validationResult.IsValid)
            {
                Console.WriteLine("✓ 証明書は有効です");
                Console.WriteLine($"  発行者: {validationResult.Issuer}");
                Console.WriteLine($"  有効期限: {validationResult.NotAfter:yyyy-MM-dd HH:mm:ss}");
            }
            else
            {
                Console.WriteLine("✗ 証明書に問題があります:");
                foreach (var error in validationResult.Errors)
                {
                    Console.WriteLine($"  - {error}");
                }
            }
        }
        else
        {
            Console.WriteLine($"証明書の取得に失敗しました: {result.ErrorMessage}");
        }
    }
}

証明書の自動更新とモニタリング

using Certes;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;

// 証明書自動更新サービス
public class CertificateRenewalService
{
    private readonly CertificateManager _certificateManager;
    private readonly System.Timers.Timer _renewalTimer;
    private readonly List<CertificateConfig> _certificates;
    private readonly string _configPath;
    private readonly CancellationTokenSource _cancellationTokenSource;

    public event EventHandler<CertificateRenewalEventArgs> CertificateRenewed;
    public event EventHandler<CertificateRenewalEventArgs> CertificateRenewalFailed;

    public CertificateRenewalService(CertificateManager certificateManager, string configPath)
    {
        _certificateManager = certificateManager;
        _configPath = configPath;
        _certificates = new List<CertificateConfig>();
        _cancellationTokenSource = new CancellationTokenSource();
        
        // 毎日午前2時に更新チェックを実行
        _renewalTimer = new System.Timers.Timer(TimeSpan.FromHours(24).TotalMilliseconds);
        _renewalTimer.Elapsed += async (sender, e) => await CheckAndRenewCertificatesAsync();
        _renewalTimer.AutoReset = true;
    }

    // 証明書設定の追加
    public void AddCertificateConfig(CertificateConfig config)
    {
        _certificates.Add(config);
    }

    // 証明書設定の読み込み
    public async Task LoadCertificateConfigsAsync()
    {
        try
        {
            if (File.Exists(_configPath))
            {
                var configJson = await File.ReadAllTextAsync(_configPath);
                var configs = System.Text.Json.JsonSerializer.Deserialize<List<CertificateConfig>>(configJson);
                _certificates.AddRange(configs);
                
                Console.WriteLine($"証明書設定を読み込みました: {_certificates.Count} 個");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"証明書設定の読み込みエラー: {ex.Message}");
        }
    }

    // 証明書設定の保存
    public async Task SaveCertificateConfigsAsync()
    {
        try
        {
            var configJson = System.Text.Json.JsonSerializer.Serialize(_certificates, new System.Text.Json.JsonSerializerOptions
            {
                WriteIndented = true
            });
            await File.WriteAllTextAsync(_configPath, configJson);
            
            Console.WriteLine("証明書設定を保存しました");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"証明書設定の保存エラー: {ex.Message}");
        }
    }

    // 自動更新サービスの開始
    public async Task StartAsync()
    {
        await LoadCertificateConfigsAsync();
        
        // 初回チェック
        await CheckAndRenewCertificatesAsync();
        
        // 定期チェック開始
        _renewalTimer.Start();
        
        Console.WriteLine("証明書自動更新サービスを開始しました");
    }

    // 自動更新サービスの停止
    public async Task StopAsync()
    {
        _renewalTimer.Stop();
        _cancellationTokenSource.Cancel();
        
        await SaveCertificateConfigsAsync();
        
        Console.WriteLine("証明書自動更新サービスを停止しました");
    }

    // 証明書の更新チェックと実行
    private async Task CheckAndRenewCertificatesAsync()
    {
        Console.WriteLine($"証明書更新チェック開始: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
        
        foreach (var config in _certificates)
        {
            try
            {
                if (_cancellationTokenSource.Token.IsCancellationRequested)
                {
                    break;
                }

                Console.WriteLine($"証明書チェック中: {config.Name}");
                
                bool needsRenewal = await _certificateManager.NeedsRenewalAsync(
                    config.CertificatePath, 
                    config.RenewalDays
                );
                
                if (needsRenewal)
                {
                    Console.WriteLine($"証明書更新が必要です: {config.Name}");
                    
                    var result = await _certificateManager.ObtainCertificateAsync(
                        config.Domains, 
                        config.CertificateType
                    );
                    
                    if (result.Success)
                    {
                        // 設定を更新
                        config.CertificatePath = result.CertificatePath;
                        config.PrivateKeyPath = result.PrivateKeyPath;
                        config.LastRenewalDate = DateTime.Now;
                        
                        Console.WriteLine($"✓ 証明書更新成功: {config.Name}");
                        
                        // イベント発火
                        CertificateRenewed?.Invoke(this, new CertificateRenewalEventArgs
                        {
                            Config = config,
                            Result = result,
                            Success = true
                        });
                        
                        // Webhook通知(オプション)
                        if (!string.IsNullOrEmpty(config.WebhookUrl))
                        {
                            await SendWebhookNotificationAsync(config, result, true);
                        }
                    }
                    else
                    {
                        Console.WriteLine($"✗ 証明書更新失敗: {config.Name} - {result.ErrorMessage}");
                        
                        // イベント発火
                        CertificateRenewalFailed?.Invoke(this, new CertificateRenewalEventArgs
                        {
                            Config = config,
                            Result = result,
                            Success = false,
                            ErrorMessage = result.ErrorMessage
                        });
                        
                        // Webhook通知(オプション)
                        if (!string.IsNullOrEmpty(config.WebhookUrl))
                        {
                            await SendWebhookNotificationAsync(config, result, false);
                        }
                    }
                }
                else
                {
                    Console.WriteLine($"証明書更新は不要です: {config.Name}");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"証明書チェックエラー: {config.Name} - {ex.Message}");
            }
        }
        
        // 設定を保存
        await SaveCertificateConfigsAsync();
        
        Console.WriteLine($"証明書更新チェック完了: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
    }

    // Webhook通知の送信
    private async Task SendWebhookNotificationAsync(CertificateConfig config, CertificateResult result, bool success)
    {
        try
        {
            using var httpClient = new System.Net.Http.HttpClient();
            
            var notification = new
            {
                certificate_name = config.Name,
                domains = config.Domains,
                success = success,
                timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                message = success ? "証明書の更新が完了しました" : $"証明書の更新に失敗しました: {result.ErrorMessage}"
            };
            
            var json = System.Text.Json.JsonSerializer.Serialize(notification);
            var content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json");
            
            var response = await httpClient.PostAsync(config.WebhookUrl, content);
            
            if (response.IsSuccessStatusCode)
            {
                Console.WriteLine($"Webhook通知送信成功: {config.Name}");
            }
            else
            {
                Console.WriteLine($"Webhook通知送信失敗: {config.Name} - {response.StatusCode}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Webhook通知エラー: {config.Name} - {ex.Message}");
        }
    }
}

// 証明書設定クラス
public class CertificateConfig
{
    public string Name { get; set; }
    public string[] Domains { get; set; }
    public CertificateType CertificateType { get; set; }
    public string CertificatePath { get; set; }
    public string PrivateKeyPath { get; set; }
    public int RenewalDays { get; set; } = 30;
    public string WebhookUrl { get; set; }
    public DateTime LastRenewalDate { get; set; }
}

// 証明書更新イベント引数
public class CertificateRenewalEventArgs : EventArgs
{
    public CertificateConfig Config { get; set; }
    public CertificateResult Result { get; set; }
    public bool Success { get; set; }
    public string ErrorMessage { get; set; }
}

// 使用例
class Program
{
    static async Task Main(string[] args)
    {
        var certificateManager = new CertificateManager(
            accountKeyPath: "account.key",
            certificateStorePath: "./certificates",
            useProduction: false
        );

        var renewalService = new CertificateRenewalService(
            certificateManager,
            configPath: "certificate_configs.json"
        );

        // イベントハンドラーの設定
        renewalService.CertificateRenewed += (sender, e) =>
        {
            Console.WriteLine($"証明書更新成功通知: {e.Config.Name}");
        };

        renewalService.CertificateRenewalFailed += (sender, e) =>
        {
            Console.WriteLine($"証明書更新失敗通知: {e.Config.Name} - {e.ErrorMessage}");
        };

        // 証明書設定の追加
        renewalService.AddCertificateConfig(new CertificateConfig
        {
            Name = "example.com",
            Domains = new[] { "example.com", "www.example.com" },
            CertificateType = CertificateType.RSA,
            RenewalDays = 30,
            WebhookUrl = "https://hooks.example.com/certificate-renewal"
        });

        // アカウントの初期化
        bool accountInitialized = await certificateManager.InitializeAccountAsync("[email protected]");
        if (!accountInitialized)
        {
            Console.WriteLine("アカウントの初期化に失敗しました");
            return;
        }

        // 自動更新サービスの開始
        await renewalService.StartAsync();

        // サービスの実行を維持
        Console.WriteLine("証明書自動更新サービスが実行中です。終了するには任意のキーを押してください...");
        Console.ReadKey();

        // サービスの停止
        await renewalService.StopAsync();
    }
}