Server certificates
QUIC always encrypts a connection, and the client checks the certificate the server presents. The check decides whether the client trusts the server. There are four ways to set it up, from the default to a development bypass.
Trust on first use
By default the client trusts a server's certificate the first time it connects to that server, and
records the certificate's fingerprint. Every later connection to the same address must present the
same certificate, or the connection is refused with an AuthenticationException.
The record is a JSON file, quic-server-pins.json, in the folder of the game's executable. Each
entry is keyed by the address and port as you passed them to Connect, and holds the SHA-256
fingerprint of the certificate in uppercase hex:
{"203.0.113.10:55237":"6487D87AEDFA7ABDAF031850DFBEB11871E487953E02E01A8DFFC86997735D20"}
Two things follow from the key:
localhostand127.0.0.1are two different servers to the client, with a record each.- A client never replaces a record on its own. A server whose certificate has changed is refused until you remove that entry from the file, and the next connection then records the new certificate. Confirm that the server really changed its certificate before you do.
A server that restarts with a different certificate therefore looks like an attack to every client that has already connected to it. Keep the server's certificate between runs; see Making a server certificate.
A certificate that chains to a root the machine already trusts is accepted without a record, if the client connects by a name the certificate was issued for. Connecting by an IP address always uses the record instead, because the name cannot match.
Your own trust rules
To decide trust yourself, set IdentityVerifier on
QuicTransportConfiguration to an
IServerIdentityVerifier. It has two methods:
GetExpectedFingerprintreturns the fingerprint you expect for the server, ornullif you have none yet, in which case the first connection is accepted.OnFingerprintObservedis called after every connection with the fingerprint the server presented, and whether it matched. Store it here if you want to remember the server.
The verifier replaces the file described above. It is consulted only for a certificate that does not pass the normal chain check. An expected fingerprint comes from somewhere you trust, such as a list that ships with your game or a directory you run.
using BSGameLib.Networking;
public class ListVerifier : IServerIdentityVerifier
{
private readonly Dictionary<string, string> _expected;
public ListVerifier(Dictionary<string, string> expected) => _expected = expected;
public string GetExpectedFingerprint(string serverKey) =>
_expected.TryGetValue(serverKey, out var fingerprint) ? fingerprint : null;
public void OnFingerprintObserved(string serverKey, string fingerprint, bool matchedExpected)
{
// Log, or show the player a warning, when matchedExpected is false.
}
}
Then give the verifier to the configuration:
using BSGameLib.Networking;
var configuration = new QuicTransportConfiguration
{
IdentityVerifier = new ListVerifier(new Dictionary<string, string>
{
["game.example.com:55237"] = "6487D87AEDFA7ABDAF031850DFBEB11871E487953E02E01A8DFFC86997735D20",
}),
};
To keep the default behaviour but in a file of your choosing, pass the path to TofuServerIdentityVerifier:
var configuration = new QuicTransportConfiguration
{
IdentityVerifier = new TofuServerIdentityVerifier(
Path.Combine(AppContext.BaseDirectory, "pins", "servers.json")),
};
Certificates from a certificate authority
A certificate from an authority the machine trusts needs no record. It is accepted when the client connects by a name that the certificate was issued for. Use the host name, not an IP address, for such a server: an IP address cannot match a name, so the client falls back to the record.
Development: skip the check
AllowUntrustedCertificates on QuicTransportConfiguration turns off
all checks of the server's certificate, and the connection is encrypted but not authenticated. The
client logs a warning on every connection. Use it on a development machine only, and never in a
shipped game. It ignores IdentityVerifier.
Making a server certificate
The server's certificate must include its private key. A server that you run yourself loads it from a PFX file:
using System.Security.Cryptography.X509Certificates;
var certificate = X509CertificateLoader.LoadPkcs12FromFile("server.pfx", "password");
For development, you can create a self-signed certificate and save it, so the server keeps the same one between runs:
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using var key = RSA.Create(2048);
var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
using var made = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
File.WriteAllBytes("server.pfx", made.Export(X509ContentType.Pfx, "password"));
The fingerprint of a certificate is the SHA-256 hash of the whole certificate, which is what the
client records and what IServerIdentityVerifier compares.
BSGameLib.Server makes and keeps a self-signed certificate for you when you do not give it one.
The ALPN protocol
Client and server must agree on an application protocol name, which QUIC negotiates during the
handshake. It is AlpnProtocol on QuicTransportConfiguration, and it
defaults to bsgamelib on both sides. Change it on both or neither: a mismatch fails the
connection.