I've been trying to get to grips with .net quic for a few days now.
In fact, it works quite well as long as I don't use certificates.
What do I want to do?
- I want to connect a server to a client
- SSL protocol runs via Http3
- I am using .net 8
- I need certificates for both sides
- The root certificate for server and client and a separate one for server and client
- The certificates are self signed for localhost via openSSL
My code looks like this:
[SupportedOSPlatform("windows")]
internal static class Program
{
// Server
private static async Task Main()
{
var rootCertificate = GetTrustStoreRootCertificate();
var serverCertificate = GetCertificate();
var chainPolicy = CreateChainPolicy(new X509Certificate2Collection(rootCertificate));
await using var listener = await QuicListener.ListenAsync(new QuicListenerOptions
{
ListenEndPoint = IPEndPoint.Parse("127.0.0.1:8889"),
ApplicationProtocols = new List<SslApplicationProtocol>
{
SslApplicationProtocol.Http3
},
ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(new QuicServerConnectionOptions
{
DefaultCloseErrorCode = 1,
DefaultStreamErrorCode = 2,
IdleTimeout = TimeSpan.FromSeconds(300),
ServerAuthenticationOptions = new SslServerAuthenticationOptions
{
ApplicationProtocols = new List<SslApplicationProtocol>
{
SslApplicationProtocol.Http3
},
ServerCertificate = serverCertificate,
ClientCertificateRequired = true,
CertificateChainPolicy = chainPolicy,
RemoteCertificateValidationCallback = (sender, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
{
return true;
}
Console.WriteLine("Certificate error: {0}", errors);
return false;
},
}
}),
});
while (true)
{
try
{
await using var connection = await listener.AcceptConnectionAsync();
await using var stream = await connection.AcceptInboundStreamAsync();
// Read
var buffer = new byte[4096];
await stream.ReadAsync(buffer);
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, buffer.Length));
// Write
await stream.WriteAsync(Encoding.UTF8.GetBytes("Pong"));
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
private static X509Certificate2 GetCertificate()
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "server.p12");
if (!File.Exists(filePath))
{
throw new FileNotFoundException("Server certificate not found");
}
var certificate = new X509Certificate2(filePath, "1412");
return certificate;
}
private static X509Certificate2 GetTrustStoreRootCertificate()
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "root.p12");
if (!File.Exists(filePath))
{
throw new FileNotFoundException("Root certificate not found");
}
var certificate = new X509Certificate2(filePath, "1412");
return certificate;
}
private static X509ChainPolicy CreateChainPolicy(X509Certificate2Collection certificate2Collection)
{
var policy = new X509ChainPolicy
{
VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority,
TrustMode = X509ChainTrustMode.CustomRootTrust
};
policy.CustomTrustStore.AddRange(certificate2Collection);
return policy;
}
}
[SupportedOSPlatform("windows")]
internal static class Program
{
// Client
private static async Task Main()
{
var rootCertificate = GetTrustStoreRootCertificate();
var clientCertificate = GetCertificate();
var chainPolicy = CreateChainPolicy(new X509Certificate2Collection(rootCertificate));
await using var connection = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions
{
RemoteEndPoint = IPEndPoint.Parse("127.0.0.1:8889"),
DefaultStreamErrorCode = 1,
DefaultCloseErrorCode = 2,
ClientAuthenticationOptions = new SslClientAuthenticationOptions
{
ApplicationProtocols = new List<SslApplicationProtocol>
{
SslApplicationProtocol.Http3
},
ClientCertificates = new X509CertificateCollection { clientCertificate },
CertificateChainPolicy = chainPolicy,
TargetHost = "localhost",
RemoteCertificateValidationCallback = (sender, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
{
return true;
}
Console.WriteLine("Certificate error: {0}", errors);
return false;
},
}
});
await using var stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
// Write
await stream.WriteAsync(Encoding.UTF8.GetBytes("Ping"));
// Read
var buffer = new byte[4096];
await stream.ReadAsync(buffer);
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, buffer.Length));
Console.ReadLine();
}
private static X509Certificate2 GetCertificate()
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "client.p12");
if (!File.Exists(filePath))
{
throw new FileNotFoundException("Client certificate not found");
}
var certificate = new X509Certificate2(filePath, "1412");
return certificate;
}
private static X509Certificate2 GetTrustStoreRootCertificate()
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "root.p12");
if (!File.Exists(filePath))
{
throw new FileNotFoundException("Root certificate not found");
}
var certificate = new X509Certificate2(filePath, "1412");
return certificate;
}
private static X509ChainPolicy CreateChainPolicy(X509Certificate2Collection certificate2Collection)
{
var policy = new X509ChainPolicy
{
VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority,
TrustMode = X509ChainTrustMode.CustomRootTrust
};
policy.CustomTrustStore.AddRange(certificate2Collection);
return policy;
}
}
I get the following error with the client when I try to connect:
System.Security.Authentication.AuthenticationException: 'The remote certificate was rejected by the provided RemoteCertificateValidationCallback.'
at System.Net.Quic.QuicConnection.SslConnectionOptions.ValidateCertificate(QUIC_BUFFER* certificatePtr, QUIC_BUFFER* chainPtr, X509Certificate2& certificate)
at System.Net.Quic.QuicConnection.HandleEventPeerCertificateReceived(_PEER_CERTIFICATE_RECEIVED_e__Struct& data)
--- End of stack trace from previous location ---
at System.Net.Quic.ValueTaskSource.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
at System.Net.Quic.QuicConnection.<FinishConnectAsync>d__31.MoveNext()
at System.Net.Quic.QuicConnection.<<ConnectAsync>g__StartConnectAsync|2_0>d.MoveNext()
at System.Net.Quic.QuicConnection.<<ConnectAsync>g__StartConnectAsync|2_0>d.MoveNext()
at System.Threading.Tasks.ValueTask`1.get_Result()
at QuicExample.Client.Program.<Main>d__0.MoveNext() in C:\Tiplu\TipluCloud.Gatekeeper\QuicExample.Client\Program.cs:line 25
I create the certificates as follows with intermediate step certificates:
echo ## Create the root certificate
openssl ecparam -out "Certificates\root.key" -genkey -name secp384r1
openssl req -new -key "Certificates\root.key" -out "Certificates\root-req.pem" -config openssl.cnf
openssl x509 -req -in "Certificates\root-req.pem" -days 3650 -signkey "Certificates\root.key" -out "Certificates\root-cert.crt"
echo ## Create the intermediate server certificate
openssl ecparam -out "Certificates\intermediate-server.key" -genkey -name secp384r1
openssl req -new -key "Certificates\intermediate-server.key" -out "Certificates\intermediate-server-req.pem" -config openssl.cnf
openssl x509 -req -in "Certificates\intermediate-server-req.pem" -days 3650 -CA "Certificates\root-cert.crt" -CAkey "Certificates\root.key" -out "Certificates\intermediate-server-cert.crt"
echo ## Create the server certificate
openssl ecparam -out "Certificates\server.key" -genkey -name secp384r1
openssl req -new -key "Certificates\server.key" -out "Certificates\server-req.pem" -config openssl.cnf
openssl x509 -req -in "Certificates\server-req.pem" -days 3650 -CA "Certificates\intermediate-server-cert.crt" -CAkey "Certificates\intermediate-server.key" -out "Certificates\server-cert.crt"
echo ## Create the intermediate client certificate
openssl ecparam -out "Certificates\intermediate-client.key" -genkey -name secp384r1
openssl req -new -key "Certificates\intermediate-client.key" -out "Certificates\intermediate-client-req.pem" -config openssl.cnf
openssl x509 -req -in "Certificates\intermediate-client-req.pem" -days 3650 -CA "Certificates\root-cert.crt" -CAkey "Certificates\root.key" -out "Certificates\intermediate-client-cert.crt"
echo ## Create the client certificate
openssl ecparam -out "Certificates\client.key" -genkey -name secp384r1
openssl req -new -key "Certificates\client.key" -out "Certificates\client-req.pem" -config openssl.cnf
openssl x509 -req -in "Certificates\client-req.pem" -days 3650 -CA "Certificates\intermediate-client-cert.crt" -CAkey "Certificates\intermediate-client.key" -out "Certificates\client-cert.crt"
echo ## Convert the certificates to PKCS#12 format
openssl pkcs12 -export -in "Certificates\root-cert.crt" -inkey "Certificates\root.key" -out "Certificates\root.p12" -passin pass:1412 -passout pass:1412
openssl pkcs12 -export -in "Certificates\server-cert.crt" -inkey "Certificates\server.key" -out "Certificates\server.p12" -passin pass:1412 -passout pass:1412
openssl pkcs12 -export -in "Certificates\client-cert.crt" -inkey "Certificates\client.key" -out "Certificates\client.p12" -passin pass:1412 -passout pass:1412
openssl.cnf
[ req ]
distinguished_name = subject
default_bits = 4096
req_extensions = req_ext
x509_extensions = x509_ext
prompt = no
encrypt_key = no
default_bits = 4096
default_md = sha256
distinguished_name = subject
req_extensions = req_ext
x509_extensions = x509_ext
string_mask = utf8only
prompt = no
encrypt_key = no
[ subject ]
countryName = EN
stateOrProvinceName = City123
localityName = City123
organizationName = Company123
organizationalUnitName = IT
commonName = localhost
emailAddress = [email protected]
[ req_ext ]
subjectKeyIdentifier = hash
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
subjectAltName = @alternate_names
nsComment = "Drone-CI - OpenSSL Generated Certificate"
[ x509_ext ]
subjectAltName = @alternate_names
seed = h4e5454aa
[ alternate_names ]
DNS.1 = localhost
Does anyone have any ideas on the best way to solve the problem?` Or does someone see a mistake that I might be overlooking?