AuthorityRoot.cs
1 using System.Buffers.Binary; 2 using System.Security.Cryptography; 3 using Org.BouncyCastle.Crypto.Parameters; 4 using Org.BouncyCastle.Math.EC.Rfc8032; 5 6 namespace GUNRPG.Security; 7 8 public sealed class AuthorityRoot 9 { 10 private readonly byte[] _publicKey; 11 private readonly RevokedServerIds _revokedServerIds; 12 13 public AuthorityRoot(byte[] publicKey, RevokedServerIds? revokedServerIds = null) 14 { 15 _publicKey = AuthorityCrypto.CloneAndValidatePublicKey(publicKey); 16 _revokedServerIds = revokedServerIds ?? RevokedServerIds.Empty; 17 } 18 19 public byte[] PublicKey => (byte[])_publicKey.Clone(); 20 21 public bool VerifyServerCertificate(ServerCertificate cert, DateTimeOffset now) 22 { 23 ArgumentNullException.ThrowIfNull(cert); 24 25 if (cert.ValidUntil <= cert.IssuedAt || cert.IssuedAt > now || cert.ValidUntil < now) 26 { 27 return false; 28 } 29 30 if (_revokedServerIds.IsRevoked(cert.ServerId)) 31 { 32 return false; 33 } 34 35 return AuthorityCrypto.VerifyHashedPayload( 36 _publicKey, 37 cert.ComputePayloadHash(), 38 cert.Signature); 39 } 40 } 41 42 internal static class AuthorityCrypto 43 { 44 internal const int KeySize = 32; 45 internal const int SignatureSize = 64; 46 private const int GuidSize = 16; 47 private const int Int64Size = 8; 48 private const int Int32Size = 4; 49 50 internal static byte[] GeneratePrivateKey() 51 { 52 return RandomNumberGenerator.GetBytes(KeySize); 53 } 54 55 internal static byte[] GetPublicKey(byte[] privateKey) 56 { 57 var normalizedPrivateKey = CloneAndValidatePrivateKey(privateKey); 58 return new Ed25519PrivateKeyParameters(normalizedPrivateKey, 0).GeneratePublicKey().GetEncoded(); 59 } 60 61 internal static byte[] SignPayload(byte[] privateKey, byte[] payload) 62 { 63 var normalizedPrivateKey = CloneAndValidatePrivateKey(privateKey); 64 ArgumentNullException.ThrowIfNull(payload); 65 66 var signature = new byte[SignatureSize]; 67 new Ed25519PrivateKeyParameters(normalizedPrivateKey, 0).Sign( 68 Ed25519.Algorithm.Ed25519, 69 null, // No RFC 8032 context: signs the raw payload bytes directly; callers pre-hash when needed. 70 payload, 71 signature); 72 return signature; 73 } 74 75 internal static bool VerifyPayload(byte[] publicKey, byte[] payload, byte[] signature) 76 { 77 var normalizedPublicKey = CloneAndValidatePublicKey(publicKey); 78 ArgumentNullException.ThrowIfNull(payload); 79 var normalizedSignature = CloneAndValidateSignature(signature); 80 81 return new Ed25519PublicKeyParameters(normalizedPublicKey, 0).Verify( 82 Ed25519.Algorithm.Ed25519, 83 null, // No RFC 8032 context: verification must match the plain Ed25519 signing mode above. 84 payload, 85 normalizedSignature); 86 } 87 88 internal static byte[] SignHashedPayload(byte[] privateKey, byte[] payloadHash) 89 { 90 var normalizedHash = CloneAndValidateSha256Hash(payloadHash); 91 return SignPayload(privateKey, normalizedHash); 92 } 93 94 internal static bool VerifyHashedPayload(byte[] publicKey, byte[] payloadHash, byte[] signature) 95 { 96 var normalizedHash = CloneAndValidateSha256Hash(payloadHash); 97 return VerifyPayload(publicKey, normalizedHash, signature); 98 } 99 100 internal static byte[] ComputeCertificatePayloadHash( 101 Guid serverId, 102 byte[] publicKey, 103 DateTimeOffset issuedAt, 104 DateTimeOffset validUntil) 105 { 106 var normalizedPublicKey = CloneAndValidatePublicKey(publicKey); 107 var buffer = new byte[GuidSize + Int32Size + normalizedPublicKey.Length + Int64Size + Int64Size]; 108 var offset = 0; 109 110 WriteGuid(serverId, buffer, ref offset); 111 WriteLengthPrefixed(normalizedPublicKey, buffer, ref offset); 112 WriteInt64(issuedAt.ToUnixTimeMilliseconds(), buffer, ref offset); 113 WriteInt64(validUntil.ToUnixTimeMilliseconds(), buffer, ref offset); 114 115 return SHA256.HashData(buffer); 116 } 117 118 internal static byte[] ComputeRunValidationPayloadHash(Guid runId, Guid playerId, byte[] finalStateHash) 119 { 120 var normalizedFinalStateHash = CloneAndValidateSha256Hash(finalStateHash); 121 var buffer = new byte[GuidSize + GuidSize + Int32Size + normalizedFinalStateHash.Length]; 122 var offset = 0; 123 124 WriteGuid(runId, buffer, ref offset); 125 WriteGuid(playerId, buffer, ref offset); 126 WriteLengthPrefixed(normalizedFinalStateHash, buffer, ref offset); 127 128 return SHA256.HashData(buffer); 129 } 130 131 internal static byte[] ComputeRunResultHash(Guid runId, Guid playerId, Guid serverId, byte[] finalStateHash) 132 { 133 var normalizedFinalStateHash = CloneAndValidateSha256Hash(finalStateHash); 134 var buffer = new byte[GuidSize + GuidSize + GuidSize + Int32Size + normalizedFinalStateHash.Length]; 135 var offset = 0; 136 137 WriteGuid(runId, buffer, ref offset); 138 WriteGuid(playerId, buffer, ref offset); 139 WriteGuid(serverId, buffer, ref offset); 140 WriteLengthPrefixed(normalizedFinalStateHash, buffer, ref offset); 141 142 return SHA256.HashData(buffer); 143 } 144 145 internal static byte[] CloneAndValidatePublicKey(byte[] publicKey) 146 { 147 ArgumentNullException.ThrowIfNull(publicKey); 148 if (publicKey.Length != KeySize) 149 { 150 throw new ArgumentException("Ed25519 public keys must be 32 bytes.", nameof(publicKey)); 151 } 152 153 return (byte[])publicKey.Clone(); 154 } 155 156 internal static byte[] CloneAndValidatePrivateKey(byte[] privateKey) 157 { 158 ArgumentNullException.ThrowIfNull(privateKey); 159 if (privateKey.Length != KeySize) 160 { 161 throw new ArgumentException("Ed25519 private keys must be 32 bytes.", nameof(privateKey)); 162 } 163 164 return (byte[])privateKey.Clone(); 165 } 166 167 internal static byte[] CloneAndValidateSignature(byte[] signature) 168 { 169 ArgumentNullException.ThrowIfNull(signature); 170 if (signature.Length != SignatureSize) 171 { 172 throw new ArgumentException("Ed25519 signatures must be 64 bytes.", nameof(signature)); 173 } 174 175 return (byte[])signature.Clone(); 176 } 177 178 internal static byte[] CloneAndValidateSha256Hash(byte[] value) 179 { 180 ArgumentNullException.ThrowIfNull(value); 181 if (value.Length != SHA256.HashSizeInBytes) 182 { 183 throw new ArgumentException($"SHA-256 hashes must be {SHA256.HashSizeInBytes} bytes.", nameof(value)); 184 } 185 186 return (byte[])value.Clone(); 187 } 188 189 private static void WriteGuid(Guid value, Span<byte> destination, ref int offset) 190 { 191 if (!value.TryWriteBytes(destination[offset..], bigEndian: true, out var bytesWritten) || bytesWritten != GuidSize) 192 { 193 throw new InvalidOperationException("Failed to write a 16-byte big-endian Guid into the signature payload buffer."); 194 } 195 196 offset += bytesWritten; 197 } 198 199 private static void WriteInt64(long value, Span<byte> destination, ref int offset) 200 { 201 BinaryPrimitives.WriteInt64BigEndian(destination[offset..], value); 202 offset += Int64Size; 203 } 204 205 private static void WriteLengthPrefixed(byte[] value, Span<byte> destination, ref int offset) 206 { 207 BinaryPrimitives.WriteInt32BigEndian(destination[offset..], value.Length); 208 offset += Int32Size; 209 value.CopyTo(destination[offset..]); 210 offset += value.Length; 211 } 212 }