76 lines
3.4 KiB
C#
76 lines
3.4 KiB
C#
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();
|
||
|
|
}
|
||
|
|
}
|