using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; namespace VoiceCat.Crypto; public sealed class ServerCredentials : IDisposable { private readonly X509Certificate2 certificate; private bool disposed; private ServerCredentials(ServerIdentity identity, X509Certificate2 certificate) { Identity = identity; this.certificate = certificate; } public ServerIdentity Identity { get; } public string CertificateFingerprint => Convert.ToHexString(SHA256.HashData(certificate.RawData)); public static ServerCredentials LoadOrCreate(string directory, string serverName) { ArgumentException.ThrowIfNullOrWhiteSpace(serverName); Directory.CreateDirectory(directory); string identityPath = Path.Combine(directory, "identity.key"); string certificatePath = Path.Combine(directory, "server.crt"); string keyPath = Path.Combine(directory, "server.key"); bool hasIdentity = File.Exists(identityPath); bool hasCertificate = File.Exists(certificatePath); bool hasKey = File.Exists(keyPath); if (hasIdentity && hasCertificate && hasKey) { var identity = ServerIdentity.Load(identityPath); try { return new(identity, X509Certificate2.CreateFromPemFile(certificatePath, keyPath)); } catch { identity.Dispose(); throw; } } if (hasIdentity || hasCertificate || hasKey) throw new InvalidDataException("Server credentials are incomplete; restore the missing files before starting."); var generated = ServerIdentity.Generate(); try { using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); var name = new X500DistinguishedNameBuilder(); name.AddCommonName(serverName); var request = new CertificateRequest(name.Build(), key, HashAlgorithmName.SHA256); request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true)); var san = new SubjectAlternativeNameBuilder(); san.AddUri(new Uri("urn:voicecat:identity:ed25519:" + Convert.ToHexString(generated.PublicKey).ToLowerInvariant())); request.CertificateExtensions.Add(san.Build()); using var created = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddYears(10)); string certificatePem = created.ExportCertificatePem(); string privateKeyPem = key.ExportPkcs8PrivateKeyPem(); generated.Save(identityPath); PrivateFiles.Write(certificatePath, Encoding.UTF8.GetBytes(certificatePem)); PrivateFiles.Write(keyPath, Encoding.UTF8.GetBytes(privateKeyPem)); return new(generated, X509Certificate2.CreateFromPem(certificatePem, privateKeyPem)); } catch { generated.Dispose(); throw; } } public TlsSession CreateTlsSession() { ObjectDisposedException.ThrowIf(disposed, this); using var key = certificate.GetECDsaPrivateKey() ?? throw new InvalidDataException("Server TLS certificate requires an ECDSA key."); return TlsSession.CreateServer(certificate.ExportCertificatePem(), key.ExportPkcs8PrivateKeyPem()); } public void Dispose() { if (disposed) return; disposed = true; Identity.Dispose(); certificate.Dispose(); } }