1using System.Security.Cryptography;
6internal static class NdsDsiIntegrityValidator
12 public static void Validate(
14 List<NdsDiagnostic> diagnostics,
15 NdsValidationOptions options)
17 NdsDsiHeader? dsi = image.Header.Dsi;
23 if (dsi.TotalImageSize != 0 &&
24 (dsi.TotalImageSize > image.Length || dsi.TotalImageSize < image.Header.UsedImageSize))
29 $
"The DSi total image size 0x{dsi.TotalImageSize:X} is inconsistent with the physical or common content size."));
32 ValidateOptionalRegion(image, diagnostics,
"NDS1302",
"first modcrypt area", dsi.ModcryptArea1);
33 ValidateOptionalRegion(image, diagnostics,
"NDS1303",
"second modcrypt area", dsi.ModcryptArea2);
34 bool digestMetadataValid = ValidateDigestMetadata(image, diagnostics, dsi, options);
35 if (!options.DsiHmacKey.IsEmpty)
37 ValidateHmacs(image, diagnostics, dsi, options.DsiHmacKey.Span);
38 if (digestMetadataValid && !dsi.SectorHashTable.IsEmpty)
40 ValidateDigestHierarchy(image, diagnostics, dsi, options, options.DsiHmacKey.Span);
44 if (options.ValidateDsiDevelopmentSignature)
46 ValidateDevelopmentSignature(image, diagnostics, dsi);
49 if (options.DsiRsaPublicKey is not
null && !dsi.VerifyRsaSignature(options.DsiRsaPublicKey))
54 "The DSi header RSA-SHA1 signature does not match the caller-trusted public key.",
65 private static void ValidateOptionalRegion(
67 List<NdsDiagnostic> diagnostics,
72 if (!region.IsEmpty && (region.Offset < 0 || region.Length < 0 || region.Offset > image.Length - region.Length))
77 $
"The DSi {name} at 0x{region.Offset:X}+0x{region.Length:X} is outside the image.",
88 private static bool ValidateDigestMetadata(
90 List<NdsDiagnostic> diagnostics,
92 NdsValidationOptions options)
94 bool hasDigestMetadata = !dsi.NtrDigest.IsEmpty || !dsi.TwlDigest.IsEmpty ||
95 !dsi.SectorHashTable.IsEmpty || !dsi.BlockHashTable.IsEmpty;
96 if (!hasDigestMetadata)
102 if (dsi.DigestSectorSize is < 0x200 or > 16 * 1024 * 1024 ||
103 (dsi.DigestSectorSize & (dsi.DigestSectorSize - 1)) != 0 ||
104 dsi.DigestBlockSectorCount is 0 or > 65_536 ||
105 dsi.SectorHashTable.IsEmpty ||
106 dsi.BlockHashTable.IsEmpty)
111 "The DSi digest hierarchy requires power-of-two sectors, a nonzero block sector count, and both hash tables."));
115 ValidateOptionalRegion(image, diagnostics,
"NDS1305",
"sector hash table", dsi.SectorHashTable);
116 ValidateOptionalRegion(image, diagnostics,
"NDS1306",
"block hash table", dsi.BlockHashTable);
117 ValidateOptionalRegion(image, diagnostics,
"NDS1307",
"NTR digest content", dsi.NtrDigest);
118 ValidateOptionalRegion(image, diagnostics,
"NDS1308",
"TWL digest content", dsi.TwlDigest);
119 if (dsi.SectorHashTable.Length > options.MaxDsiDigestTableBytes ||
120 dsi.BlockHashTable.Length > options.MaxDsiDigestTableBytes)
125 "A DSi digest table exceeds the configured validation allocation limit."));
129 if (!valid || !IsWithin(image, dsi.NtrDigest) || !IsWithin(image, dsi.TwlDigest) ||
130 !IsWithin(image, dsi.SectorHashTable) || !IsWithin(image, dsi.BlockHashTable))
136 long expectedSectorBytes;
138 long expectedBlockBytes;
141 sectorCount = checked(
142 DivideRoundUp(dsi.NtrDigest.Length, dsi.DigestSectorSize) +
143 DivideRoundUp(dsi.TwlDigest.Length, dsi.DigestSectorSize));
144 expectedSectorBytes = checked(sectorCount * 20);
145 blockCount = DivideRoundUp(sectorCount, dsi.DigestBlockSectorCount);
146 expectedBlockBytes = checked(blockCount * 20);
148 catch (OverflowException)
153 "The DSi digest hierarchy overflows supported sector or table counts."));
157 if (dsi.SectorHashTable.Length != expectedSectorBytes || dsi.BlockHashTable.Length != expectedBlockBytes)
162 $
"DSi digest table lengths do not match {sectorCount} content sectors and {blockCount} hash blocks."));
175 private static void ValidateDigestHierarchy(
177 List<NdsDiagnostic> diagnostics,
179 NdsValidationOptions options,
180 ReadOnlySpan<byte> key)
182 byte[] sectorHashes = ReadRegion(image, dsi.SectorHashTable);
183 byte[] blockHashes = ReadRegion(image, dsi.BlockHashTable);
184 int failureCount = 0;
186 ValidateRegionSectors(
190 dsi.DigestSectorSize,
194 options.MaxDsiDigestFailures,
196 ValidateRegionSectors(
200 dsi.DigestSectorSize,
204 options.MaxDsiDigestFailures,
207 int blockInputSize = checked((
int)dsi.DigestBlockSectorCount * 20);
208 for (
int blockIndex = 0; blockIndex * blockInputSize < sectorHashes.Length; blockIndex++)
210 int inputOffset = checked(blockIndex * blockInputSize);
211 ReadOnlySpan<byte> input = sectorHashes.AsSpan(
213 Math.Min(blockInputSize, sectorHashes.Length - inputOffset));
214#pragma warning disable CA5350
215 byte[] calculated = HMACSHA1.HashData(key, input);
216#pragma warning restore CA5350
217 if (!CryptographicOperations.FixedTimeEquals(blockHashes.AsSpan(blockIndex * 20, 20), calculated) &&
218 failureCount++ < options.MaxDsiDigestFailures)
223 $
"DSi digest block {blockIndex} does not authenticate its sector-hash group.",
224 new(dsi.BlockHashTable.Offset + (blockIndex * 20L), 20)));
228#pragma warning disable CA5350
229 byte[] master = HMACSHA1.HashData(key, blockHashes);
230#pragma warning restore CA5350
231 if (!CryptographicOperations.FixedTimeEquals(dsi.DigestMasterHmac.Span, master))
236 "The DSi digest master HMAC does not authenticate the block hash table.",
240 if (failureCount > options.MaxDsiDigestFailures)
245 $
"Additional DSi digest mismatches were suppressed after {options.MaxDsiDigestFailures} findings."));
259 private static void ValidateRegionSectors(
261 List<NdsDiagnostic> diagnostics,
266 ref
int failureCount,
268 ReadOnlySpan<byte> key)
271 while (offset < region.Length)
273 long length = Math.Min(sectorSize, region.Length - offset);
274 var sector =
new NdsRegion(region.Offset + offset, length);
275 byte[] calculated = CalculateRegionHmac(image, sector, key);
276 if (!CryptographicOperations.FixedTimeEquals(storedHashes.AsSpan(sectorIndex * 20, 20), calculated) &&
277 failureCount++ < failureLimit)
282 $
"DSi digest sector {sectorIndex} does not match its covered image bytes.",
296 private static void ValidateHmacs(
298 List<NdsDiagnostic> diagnostics,
300 ReadOnlySpan<byte> key)
302 ValidateHmac(image, diagnostics,
"NDS1310",
"ARM9", image.Header.Arm9.Data, dsi.Arm9Hmac.Span, key);
303 ValidateHmac(image, diagnostics,
"NDS1311",
"ARM7", image.Header.Arm7.Data, dsi.Arm7Hmac.Span, key);
304 var banner =
new NdsRegion(image.Header.BannerOffset, dsi.BannerSize);
305 ValidateHmac(image, diagnostics,
"NDS1312",
"Banner", banner, dsi.BannerHmac.Span, key);
306 ValidateHmac(image, diagnostics,
"NDS1313",
"ARM9i", image.Header.Arm9i!.Data, dsi.Arm9iHmac.Span, key);
307 ValidateHmac(image, diagnostics,
"NDS1314",
"ARM7i", image.Header.Arm7i!.Data, dsi.Arm7iHmac.Span, key);
318 private static void ValidateHmac(
320 List<NdsDiagnostic> diagnostics,
324 ReadOnlySpan<byte> stored,
325 ReadOnlySpan<byte> key)
327 if (region.Offset < 0 || region.Length < 0 || region.Offset > image.Length - region.Length)
332 byte[] calculated = CalculateRegionHmac(image, region, key);
333 if (!CryptographicOperations.FixedTimeEquals(stored, calculated))
338 $
"The DSi {name} HMAC-SHA1 does not match the supplied key.",
348 private static byte[] CalculateRegionHmac(NdsImage image, NdsRegion region, ReadOnlySpan<byte> key)
350#pragma warning disable CA5350
351 using IncrementalHash hash = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA1, key);
352#pragma warning restore CA5350
353 using Stream stream = image.OpenRead(region);
354 byte[] buffer =
new byte[64 * 1024];
356 while ((read = stream.Read(buffer)) > 0)
358 hash.AppendData(buffer, 0, read);
361 return hash.GetHashAndReset();
368 private static byte[] ReadRegion(NdsImage image, NdsRegion region)
370 byte[] data =
new byte[checked((
int)region.Length)];
371 using Stream stream = image.OpenRead(region);
372 stream.ReadExactly(data);
380 private static bool IsWithin(NdsImage image, NdsRegion region) =>
381 region.Offset >= 0 && region.Length >= 0 && region.Offset <= image.Length - region.Length;
387 private static long DivideRoundUp(
long value, uint divisor) => checked((value + divisor - 1) / divisor);
393 private static void ValidateDevelopmentSignature(
395 List<NdsDiagnostic> diagnostics,
398 ReadOnlySpan<byte> signature = dsi.RsaSignature.Span;
399 if (signature[0] != 0 || signature[1] != 1 || signature[0x6B] != 0)
404#pragma warning disable CA5350
405 byte[] calculated = SHA1.HashData(image.Header.RawData.Span[..0xE00]);
406#pragma warning restore CA5350
407 if (!CryptographicOperations.FixedTimeEquals(signature[0x6C..0x80], calculated))
412 "The no$gba DSi development marker does not match the finalized extended header.",
NdsDiagnosticSeverity
Indicates the impact of a validation finding.