using System; using GitHub.Runner.Common.Util; using System.IO; using System.Runtime.Serialization; using GitHub.Services.Common; using System.Security.Cryptography.X509Certificates; using System.Net; using System.Net.Security; using System.Net.Http; using GitHub.Services.WebApi; using GitHub.Runner.Sdk; namespace GitHub.Runner.Common { [ServiceLocator(Default = typeof(RunnerCertificateManager))] public interface IRunnerCertificateManager : IRunnerService { bool SkipServerCertificateValidation { get; } string CACertificateFile { get; } string ClientCertificateFile { get; } string ClientCertificatePrivateKeyFile { get; } string ClientCertificateArchiveFile { get; } string ClientCertificatePassword { get; } IVssClientCertificateManager VssClientCertificateManager { get; } } public class RunnerCertificateManager : RunnerService, IRunnerCertificateManager { private RunnerClientCertificateManager _runnerClientCertificateManager = new RunnerClientCertificateManager(); public bool SkipServerCertificateValidation { private set; get; } public string CACertificateFile { private set; get; } public string ClientCertificateFile { private set; get; } public string ClientCertificatePrivateKeyFile { private set; get; } public string ClientCertificateArchiveFile { private set; get; } public string ClientCertificatePassword { private set; get; } public IVssClientCertificateManager VssClientCertificateManager => _runnerClientCertificateManager; public override void Initialize(IHostContext hostContext) { base.Initialize(hostContext); LoadCertificateSettings(); } // This should only be called from config public void SetupCertificate(bool skipCertValidation, string caCert, string clientCert, string clientCertPrivateKey, string clientCertArchive, string clientCertPassword) { Trace.Info("Setup runner certificate setting base on configuration inputs."); if (skipCertValidation) { Trace.Info("Ignore SSL server certificate validation error"); SkipServerCertificateValidation = true; VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; } if (!string.IsNullOrEmpty(caCert)) { ArgUtil.File(caCert, nameof(caCert)); Trace.Info($"Self-Signed CA '{caCert}'"); } if (!string.IsNullOrEmpty(clientCert)) { ArgUtil.File(clientCert, nameof(clientCert)); ArgUtil.File(clientCertPrivateKey, nameof(clientCertPrivateKey)); ArgUtil.File(clientCertArchive, nameof(clientCertArchive)); Trace.Info($"Client cert '{clientCert}'"); Trace.Info($"Client cert private key '{clientCertPrivateKey}'"); Trace.Info($"Client cert archive '{clientCertArchive}'"); } CACertificateFile = caCert; ClientCertificateFile = clientCert; ClientCertificatePrivateKeyFile = clientCertPrivateKey; ClientCertificateArchiveFile = clientCertArchive; ClientCertificatePassword = clientCertPassword; _runnerClientCertificateManager.AddClientCertificate(ClientCertificateArchiveFile, ClientCertificatePassword); } // This should only be called from config public void SaveCertificateSetting() { string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates); IOUtil.DeleteFile(certSettingFile); var setting = new RunnerCertificateSetting(); if (SkipServerCertificateValidation) { Trace.Info($"Store Skip ServerCertificateValidation setting to '{certSettingFile}'"); setting.SkipServerCertValidation = true; } if (!string.IsNullOrEmpty(CACertificateFile)) { Trace.Info($"Store CA cert setting to '{certSettingFile}'"); setting.CACert = CACertificateFile; } if (!string.IsNullOrEmpty(ClientCertificateFile) && !string.IsNullOrEmpty(ClientCertificatePrivateKeyFile) && !string.IsNullOrEmpty(ClientCertificateArchiveFile)) { Trace.Info($"Store client cert settings to '{certSettingFile}'"); setting.ClientCert = ClientCertificateFile; setting.ClientCertPrivatekey = ClientCertificatePrivateKeyFile; setting.ClientCertArchive = ClientCertificateArchiveFile; if (!string.IsNullOrEmpty(ClientCertificatePassword)) { string lookupKey = Guid.NewGuid().ToString("D").ToUpperInvariant(); Trace.Info($"Store client cert private key password with lookup key {lookupKey}"); var credStore = HostContext.GetService(); credStore.Write($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{lookupKey}", "GitHub", ClientCertificatePassword); setting.ClientCertPasswordLookupKey = lookupKey; } } if (SkipServerCertificateValidation || !string.IsNullOrEmpty(CACertificateFile) || !string.IsNullOrEmpty(ClientCertificateFile)) { IOUtil.SaveObject(setting, certSettingFile); File.SetAttributes(certSettingFile, File.GetAttributes(certSettingFile) | FileAttributes.Hidden); } } // This should only be called from unconfig public void DeleteCertificateSetting() { string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates); if (File.Exists(certSettingFile)) { Trace.Info($"Load runner certificate setting from '{certSettingFile}'"); var certSetting = IOUtil.LoadObject(certSettingFile); if (certSetting != null && !string.IsNullOrEmpty(certSetting.ClientCertPasswordLookupKey)) { Trace.Info("Delete client cert private key password from credential store."); var credStore = HostContext.GetService(); credStore.Delete($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{certSetting.ClientCertPasswordLookupKey}"); } Trace.Info($"Delete cert setting file: {certSettingFile}"); IOUtil.DeleteFile(certSettingFile); } } public void LoadCertificateSettings() { string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates); if (File.Exists(certSettingFile)) { Trace.Info($"Load runner certificate setting from '{certSettingFile}'"); var certSetting = IOUtil.LoadObject(certSettingFile); ArgUtil.NotNull(certSetting, nameof(RunnerCertificateSetting)); if (certSetting.SkipServerCertValidation) { Trace.Info("Ignore SSL server certificate validation error"); SkipServerCertificateValidation = true; VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; } if (!string.IsNullOrEmpty(certSetting.CACert)) { // make sure all settings file exist ArgUtil.File(certSetting.CACert, nameof(certSetting.CACert)); Trace.Info($"CA '{certSetting.CACert}'"); CACertificateFile = certSetting.CACert; } if (!string.IsNullOrEmpty(certSetting.ClientCert)) { // make sure all settings file exist ArgUtil.File(certSetting.ClientCert, nameof(certSetting.ClientCert)); ArgUtil.File(certSetting.ClientCertPrivatekey, nameof(certSetting.ClientCertPrivatekey)); ArgUtil.File(certSetting.ClientCertArchive, nameof(certSetting.ClientCertArchive)); Trace.Info($"Client cert '{certSetting.ClientCert}'"); Trace.Info($"Client cert private key '{certSetting.ClientCertPrivatekey}'"); Trace.Info($"Client cert archive '{certSetting.ClientCertArchive}'"); ClientCertificateFile = certSetting.ClientCert; ClientCertificatePrivateKeyFile = certSetting.ClientCertPrivatekey; ClientCertificateArchiveFile = certSetting.ClientCertArchive; if (!string.IsNullOrEmpty(certSetting.ClientCertPasswordLookupKey)) { var cerdStore = HostContext.GetService(); ClientCertificatePassword = cerdStore.Read($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{certSetting.ClientCertPasswordLookupKey}").Password; HostContext.SecretMasker.AddValue(ClientCertificatePassword); } _runnerClientCertificateManager.AddClientCertificate(ClientCertificateArchiveFile, ClientCertificatePassword); } } else { Trace.Info("No certificate setting found."); } } } [DataContract] internal class RunnerCertificateSetting { [DataMember] public bool SkipServerCertValidation { get; set; } [DataMember] public string CACert { get; set; } [DataMember] public string ClientCert { get; set; } [DataMember] public string ClientCertPrivatekey { get; set; } [DataMember] public string ClientCertArchive { get; set; } [DataMember] public string ClientCertPasswordLookupKey { get; set; } } }