using System; using System.Globalization; using System.Text; namespace VanadiumGuard.Protocol { /// What is holding the private half of the attestation key. public enum AttestationBacking { /// No key at all. The session falls back to HMAC only. None = 0, /// /// A TPM, through the Windows Platform Crypto Provider. The private key physically /// cannot leave the chip, so a signature proves the request was made on this machine. /// Tpm = 1, /// /// A software key held by the OS key store. Better than nothing for continuity, but /// it is extractable by anyone with the same access the guard has, so the server /// treats it as unattested. /// Software = 2, } /// /// A device key and the proof that this client holds its private half, presented once at /// session open. /// /// This exists because of the limit stated in the README: the HMAC session key lives in /// the client process, so anyone with a debugger can lift it and sign whatever they like. /// Replay, sequence and nonce checks do not help there - the attacker is not replaying /// anything, they are minting fresh, correctly-ordered requests. A TPM key is the one /// thing on a consumer machine that a debugger cannot walk off with: it signs inside the /// chip and the private half never exists in process memory. /// /// /// It does not make the client trustworthy. An attacker on the machine can still ask the /// TPM to sign things, exactly as the guard does. What it removes is the ability to lift /// a key and forge that machine traffic from somewhere else, and it binds every request /// to a piece of hardware that can be banned. /// /// public sealed class DeviceAttestation { public AttestationBacking Backing { get; set; } = AttestationBacking.None; /// Base64 SubjectPublicKeyInfo for the ECDSA P-256 public key. public string PublicKey { get; set; } = string.Empty; /// Named so a future key type does not silently verify under the wrong rules. public string Algorithm { get; set; } = AlgorithmEs256; public long TimestampUnixMs { get; set; } public string Nonce { get; set; } = string.Empty; /// Base64 signature over , IEEE P1363 form. public string ProofSignature { get; set; } = string.Empty; public const string AlgorithmEs256 = "ES256"; public bool IsPresent => Backing != AttestationBacking.None && PublicKey.Length > 0; /// /// The string the proof signature covers. Lives here, next to the canonical string /// for signed requests, so the two ends cannot drift apart. /// /// The player id and the build are bound in so a proof captured from one session /// cannot be presented for a different account. The nonce is chosen by the client /// rather than issued by the server, which would normally be a weakness - but a /// stolen proof buys nothing on its own here, because every subsequent request has to /// be signed by the same private key, and that key is in the chip. /// /// public static string BuildProofString( string playerId, string gameBuild, long timestampUnixMs, string nonce, string publicKeyBase64) { var sb = new StringBuilder(); sb.Append("VG-ATTEST-V1").Append('\n'); sb.Append(playerId ?? string.Empty).Append('\n'); sb.Append(gameBuild ?? string.Empty).Append('\n'); sb.Append(timestampUnixMs.ToString(CultureInfo.InvariantCulture)).Append('\n'); sb.Append(nonce ?? string.Empty).Append('\n'); sb.Append(publicKeyBase64 ?? string.Empty); return sb.ToString(); } /// /// Short, stable identifier for the key, for logs and ban records. A hash rather than /// the key itself so a log line does not carry an eighty character blob. /// public static string Thumbprint(string publicKeyBase64) { if (string.IsNullOrEmpty(publicKeyBase64)) return string.Empty; using (var sha = System.Security.Cryptography.SHA256.Create()) { byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(publicKeyBase64)); return Convert.ToHexString(hash).ToLowerInvariant().Substring(0, 32); } } } }