Add managed TLS interoperability and persisted credentials
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user