303 lines
8.4 KiB
C#
303 lines
8.4 KiB
C#
using System.Security.Cryptography;
|
||
using System.Security.Cryptography.X509Certificates;
|
||
using ZA.CoreService.ESBCertificateManager.Models;
|
||
|
||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||
|
||
public sealed class CertificateProbeService
|
||
{
|
||
public static readonly string[] SupportedExtensions =
|
||
[
|
||
".cer",
|
||
".crt",
|
||
".pem",
|
||
".pfx",
|
||
".p12"
|
||
];
|
||
|
||
public CertificateProbeResult Probe(
|
||
string certificatePath,
|
||
Func<string?>? passwordPrompt = null)
|
||
{
|
||
string? normalized = NormalizePath(certificatePath);
|
||
|
||
if (normalized is null)
|
||
{
|
||
return CertificateProbeResult.Failed(
|
||
"Kein gültiger Zertifikatspfad angegeben.");
|
||
}
|
||
|
||
if (!IsSupportedExtension(normalized))
|
||
{
|
||
return CertificateProbeResult.Failed(
|
||
"Nicht unterstütztes Format. Erlaubt: "
|
||
+ string.Join(", ", SupportedExtensions));
|
||
}
|
||
|
||
if (!File.Exists(normalized))
|
||
{
|
||
return CertificateProbeResult.Missing(
|
||
normalized,
|
||
"Am Zielpfad liegt noch keine Zertifikatsdatei.");
|
||
}
|
||
|
||
try
|
||
{
|
||
CertificateInfo info = ReadCertificate(normalized, password: null);
|
||
return CertificateProbeResult.Found(normalized, info);
|
||
}
|
||
catch (CryptographicException) when (IsPasswordProtected(normalized))
|
||
{
|
||
if (passwordPrompt is null)
|
||
{
|
||
return CertificateProbeResult.PasswordRequired(normalized);
|
||
}
|
||
|
||
string? password = passwordPrompt.Invoke();
|
||
|
||
if (password is null)
|
||
{
|
||
return CertificateProbeResult.Failed(
|
||
"Zum Lesen der PFX/P12-Datei wird ein Kennwort benötigt.");
|
||
}
|
||
|
||
try
|
||
{
|
||
CertificateInfo info = ReadCertificate(normalized, password);
|
||
return CertificateProbeResult.Found(normalized, info);
|
||
}
|
||
catch (CryptographicException)
|
||
{
|
||
return CertificateProbeResult.Failed(
|
||
"Das Kennwort ist falsch oder die Datei ist beschädigt.");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return CertificateProbeResult.Failed(
|
||
$"Zertifikat konnte nicht gelesen werden: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
public static bool TrySplitTargetPath(
|
||
string fullPath,
|
||
out string directory,
|
||
out string fileName,
|
||
out string? error)
|
||
{
|
||
directory = string.Empty;
|
||
fileName = string.Empty;
|
||
error = null;
|
||
|
||
string? normalized = NormalizePath(fullPath);
|
||
|
||
if (normalized is null)
|
||
{
|
||
error = "Bitte einen vollständigen Pfad zur Zertifikatsdatei angeben.";
|
||
return false;
|
||
}
|
||
|
||
if (!IsSupportedExtension(normalized))
|
||
{
|
||
error =
|
||
"Dateiendung nicht unterstützt. Erlaubt: "
|
||
+ string.Join(", ", SupportedExtensions);
|
||
return false;
|
||
}
|
||
|
||
string? dir = Path.GetDirectoryName(normalized);
|
||
string file = Path.GetFileName(normalized);
|
||
|
||
if (string.IsNullOrWhiteSpace(dir) || string.IsNullOrWhiteSpace(file))
|
||
{
|
||
error =
|
||
"Pfad muss Verzeichnis und Dateiname enthalten, z. B.\n"
|
||
+ @"\\server\e$\...\certs\server.p12";
|
||
return false;
|
||
}
|
||
|
||
if (Path.GetFileName(file) != file)
|
||
{
|
||
error = "Dateiname darf keinen weiteren Verzeichnisanteil enthalten.";
|
||
return false;
|
||
}
|
||
|
||
directory = dir;
|
||
fileName = file;
|
||
return true;
|
||
}
|
||
|
||
public static string CombineTargetPath(
|
||
string directory,
|
||
string fileName)
|
||
{
|
||
return Path.Combine(
|
||
directory.Trim().TrimEnd('\\', '/'),
|
||
fileName.Trim());
|
||
}
|
||
|
||
public static bool IsSupportedExtension(string path)
|
||
{
|
||
string extension = Path.GetExtension(path);
|
||
return SupportedExtensions.Contains(
|
||
extension,
|
||
StringComparer.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private static bool IsPasswordProtected(string path)
|
||
{
|
||
string extension = Path.GetExtension(path);
|
||
return extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
|
||
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private static string? NormalizePath(string? path)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
string trimmed = path.Trim().Trim('"');
|
||
|
||
if (trimmed.Length == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// UNC und absolute lokale Pfade akzeptieren
|
||
if (!Path.IsPathRooted(trimmed)
|
||
&& !trimmed.StartsWith(@"\\", StringComparison.Ordinal))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return trimmed;
|
||
}
|
||
|
||
private static CertificateInfo ReadCertificate(
|
||
string certificatePath,
|
||
string? password)
|
||
{
|
||
string extension = Path.GetExtension(certificatePath);
|
||
|
||
using X509Certificate2 certificate =
|
||
IsPasswordProtected(certificatePath)
|
||
? new X509Certificate2(
|
||
certificatePath,
|
||
password,
|
||
X509KeyStorageFlags.EphemeralKeySet)
|
||
: new X509Certificate2(certificatePath);
|
||
|
||
string subject = certificate.GetNameInfo(
|
||
X509NameType.SimpleName,
|
||
forIssuer: false);
|
||
|
||
string issuer = certificate.GetNameInfo(
|
||
X509NameType.SimpleName,
|
||
forIssuer: true);
|
||
|
||
if (string.IsNullOrWhiteSpace(subject))
|
||
{
|
||
subject = certificate.Subject;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(issuer))
|
||
{
|
||
issuer = certificate.Issuer;
|
||
}
|
||
|
||
string fingerprint = certificate.GetCertHashString(
|
||
HashAlgorithmName.SHA256);
|
||
|
||
fingerprint = string.Join(
|
||
":",
|
||
Enumerable.Range(0, fingerprint.Length / 2)
|
||
.Select(index => fingerprint.Substring(index * 2, 2)));
|
||
|
||
DateTimeOffset validFrom = new(certificate.NotBefore);
|
||
DateTimeOffset validUntil = new(certificate.NotAfter);
|
||
DateTimeOffset now = DateTimeOffset.Now;
|
||
|
||
return new CertificateInfo
|
||
{
|
||
Subject = subject,
|
||
Issuer = issuer,
|
||
FingerprintSha256 = fingerprint,
|
||
ValidFrom = validFrom,
|
||
ValidUntil = validUntil,
|
||
IsCurrentlyValid = now >= validFrom && now <= validUntil
|
||
};
|
||
}
|
||
}
|
||
|
||
public sealed class CertificateProbeResult
|
||
{
|
||
public bool Success { get; init; }
|
||
|
||
public bool FileExists { get; init; }
|
||
|
||
public string Path { get; init; } = string.Empty;
|
||
|
||
public string Message { get; init; } = string.Empty;
|
||
|
||
public CertificateInfo? Certificate { get; init; }
|
||
|
||
public static CertificateProbeResult Found(
|
||
string path,
|
||
CertificateInfo certificate)
|
||
{
|
||
string expiry = certificate.ValidUntil.ToLocalTime()
|
||
.ToString("dd.MM.yyyy HH:mm");
|
||
|
||
string validity = certificate.IsCurrentlyValid
|
||
? "gültig"
|
||
: "abgelaufen / ungültig";
|
||
|
||
return new CertificateProbeResult
|
||
{
|
||
Success = true,
|
||
FileExists = true,
|
||
Path = path,
|
||
Certificate = certificate,
|
||
Message =
|
||
$"Zertifikat gefunden · Subject: {certificate.Subject} · "
|
||
+ $"Ablauf: {expiry} ({validity})"
|
||
};
|
||
}
|
||
|
||
public static CertificateProbeResult Missing(
|
||
string path,
|
||
string message)
|
||
{
|
||
return new CertificateProbeResult
|
||
{
|
||
Success = true,
|
||
FileExists = false,
|
||
Path = path,
|
||
Message = message
|
||
};
|
||
}
|
||
|
||
public static CertificateProbeResult Failed(string message)
|
||
{
|
||
return new CertificateProbeResult
|
||
{
|
||
Success = false,
|
||
FileExists = false,
|
||
Message = message
|
||
};
|
||
}
|
||
|
||
public static CertificateProbeResult PasswordRequired(string path)
|
||
{
|
||
return new CertificateProbeResult
|
||
{
|
||
Success = false,
|
||
FileExists = true,
|
||
Path = path,
|
||
Message = "PFX/P12 vorhanden – Kennwort nötig zum Lesen des Ablaufs"
|
||
};
|
||
}
|
||
}
|