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 }