using System; using System.Security.Cryptography; using System.Text; using VanadiumGuard.Protocol; namespace VanadiumGuard.Server.Security { /// Why an attestation was rejected, for logs and detections. public enum AttestationResult { /// Proof verified. The key is bound to the session. Ok, /// No attestation offered. Falls back to HMAC-only; not an error. Absent, /// Key material was unusable, or the algorithm was not one we verify. Malformed, /// The proof did not verify under the offered key. BadProof, /// The proof was too old to accept. Stale, } /// /// Verifies device attestations at session open, and the per-request signatures that /// follow. /// /// Worth being precise about what this buys, because it is easy to oversell. It does not /// make the client honest: an attacker sitting on the machine can ask the TPM to sign /// whatever the guard would have signed. What it removes is the ability to extract a key /// and drive a session from somewhere else, which is what the HMAC scheme cannot stop, /// and it pins every request to one piece of hardware that can then be banned. /// /// public static class AttestationVerifier { /// /// How old a proof of possession may be. Tighter than the request clock skew because /// this is presented once, at the start, and has no sequence number behind it. /// public static readonly TimeSpan MaxProofAge = TimeSpan.FromMinutes(5); /// /// Checks the proof of possession presented at session open. Returns Absent, not an /// error, when no attestation was offered. /// public static AttestationResult VerifyProof( DeviceAttestation? attestation, string playerId, string gameBuild) { if (attestation == null || !attestation.IsPresent) return AttestationResult.Absent; if (!string.Equals(attestation.Algorithm, DeviceAttestation.AlgorithmEs256, StringComparison.Ordinal)) return AttestationResult.Malformed; double age = Math.Abs( (DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeMilliseconds(attestation.TimestampUnixMs)) .TotalSeconds); if (age > MaxProofAge.TotalSeconds) return AttestationResult.Stale; string proof = DeviceAttestation.BuildProofString( playerId, gameBuild, attestation.TimestampUnixMs, attestation.Nonce, attestation.PublicKey); return Verify(attestation.PublicKey, proof, attestation.ProofSignature) ? AttestationResult.Ok : AttestationResult.BadProof; } /// /// Verifies one signature made by a bound device key over the canonical request /// string. Returns false for anything it cannot check, including unusable key /// material - a key that stops parsing mid-session is not something to wave through. /// public static bool Verify(string publicKeyBase64, string payload, string signatureBase64) { if (string.IsNullOrEmpty(publicKeyBase64) || string.IsNullOrEmpty(signatureBase64)) return false; try { byte[] spki = Convert.FromBase64String(publicKeyBase64); byte[] signature = Convert.FromBase64String(signatureBase64); using ECDsa key = ECDsa.Create(); key.ImportSubjectPublicKeyInfo(spki, out _); // Default signature format on both ends is IEEE P1363 fixed-field, which is // what ECDsa.SignData produces on the client. Do not switch one side to DER // without switching the other. return key.VerifyData( Encoding.UTF8.GetBytes(payload), signature, HashAlgorithmName.SHA256); } catch (Exception) { // Bad base64, a key that is not P-256, a truncated SPKI. All the same answer. return false; } } } }