/ GUNRPG.Infrastructure / Security / AuthorityRoot.cs
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  }