/ GUNRPG.Infrastructure / Security / SignedRunValidation.cs
SignedRunValidation.cs
  1  using System.Buffers.Binary;
  2  using System.Security.Cryptography;
  3  
  4  namespace GUNRPG.Security;
  5  
  6  public sealed class SignedRunValidation
  7  {
  8      public SignedRunValidation(
  9          RunValidationSignature validation,
 10          ServerCertificate certificate)
 11      {
 12          Validation = validation ?? throw new ArgumentNullException(nameof(validation));
 13          Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
 14      }
 15  
 16      public RunValidationSignature Validation { get; }
 17  
 18      public ServerCertificate Certificate { get; }
 19  
 20      public List<AuthoritySignature> Signatures { get; init; } = [];
 21  
 22      internal byte[] ComputeResultHash() => RunValidationResult.ComputeResultHash(Validation);
 23  
 24      public static SignedRunValidation MergeSignatures(
 25          SignedRunValidation a,
 26          SignedRunValidation b)
 27      {
 28          ArgumentNullException.ThrowIfNull(a);
 29          ArgumentNullException.ThrowIfNull(b);
 30  
 31          var aResultHash = a.ComputeResultHash();
 32          var bResultHash = b.ComputeResultHash();
 33          if (!CryptographicOperations.FixedTimeEquals(aResultHash, bResultHash))
 34          {
 35              throw new ArgumentException("Signed validations must represent the same result to merge signatures.", nameof(b));
 36          }
 37  
 38          if (!HasMatchingAttestationMaterial(a, b))
 39          {
 40              throw new ArgumentException("Signed validations must have identical attestation material to merge signatures.", nameof(b));
 41          }
 42  
 43          var mergedSignatures = new List<AuthoritySignature>();
 44          var seenSigners = new HashSet<string>(StringComparer.Ordinal);
 45  
 46          AddValidUniqueSignatures(a.Signatures, aResultHash, seenSigners, mergedSignatures);
 47          AddValidUniqueSignatures(b.Signatures, aResultHash, seenSigners, mergedSignatures);
 48  
 49          return new SignedRunValidation(a.Validation, a.Certificate)
 50          {
 51              Signatures = mergedSignatures
 52          };
 53      }
 54  
 55      private static void AddValidUniqueSignatures(
 56          IEnumerable<AuthoritySignature> signatures,
 57          byte[] resultHash,
 58          HashSet<string> seenSigners,
 59          List<AuthoritySignature> mergedSignatures)
 60      {
 61          var signatureIndex = 0;
 62          foreach (var signature in signatures)
 63          {
 64              if (signature is null)
 65              {
 66                  throw new ArgumentException($"Signature collections must not contain null entries (index {signatureIndex}).", nameof(signatures));
 67              }
 68  
 69              signatureIndex++;
 70  
 71              var signerId = AuthoritySet.CreateKeyIdentifier(signature.PublicKeyBytes);
 72              if (!seenSigners.Add(signerId))
 73              {
 74                  continue;
 75              }
 76  
 77              if (!AuthorityCrypto.VerifyHashedPayload(
 78                      signature.PublicKeyBytes,
 79                      resultHash,
 80                      signature.SignatureBytes))
 81              {
 82                  continue;
 83              }
 84  
 85              mergedSignatures.Add(signature);
 86          }
 87      }
 88  
 89      private static bool HasMatchingAttestationMaterial(
 90          SignedRunValidation a,
 91          SignedRunValidation b)
 92      {
 93          return CryptographicOperations.FixedTimeEquals(a.Validation.SignatureBytes, b.Validation.SignatureBytes)
 94              && a.Certificate.ServerId == b.Certificate.ServerId
 95              && FixedTimeEqualsTimestamp(a.Certificate.IssuedAt, b.Certificate.IssuedAt)
 96              && FixedTimeEqualsTimestamp(a.Certificate.ValidUntil, b.Certificate.ValidUntil)
 97              && CryptographicOperations.FixedTimeEquals(a.Certificate.PublicKeyBytes, b.Certificate.PublicKeyBytes)
 98              && CryptographicOperations.FixedTimeEquals(a.Certificate.SignatureBytes, b.Certificate.SignatureBytes);
 99      }
100  
101      private static bool FixedTimeEqualsTimestamp(DateTimeOffset a, DateTimeOffset b)
102      {
103          Span<byte> left = stackalloc byte[sizeof(long)];
104          Span<byte> right = stackalloc byte[sizeof(long)];
105          BinaryPrimitives.WriteInt64BigEndian(left, a.ToUnixTimeMilliseconds());
106          BinaryPrimitives.WriteInt64BigEndian(right, b.ToUnixTimeMilliseconds());
107          return CryptographicOperations.FixedTimeEquals(left, right);
108      }
109  }