From 1f0c82c944b28f04471c47d4800134dfa8f65719 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:12:26 +0000 Subject: [PATCH 1/9] Implement seed-based deterministic strong name key generation Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .github/workflows/snk.yml | 31 --- Readme.md | 16 +- Snek.Demo/Snek.Demo.csproj | 9 + Snek/GenerateStrongNameKeyPairTask.cs | 63 ++++++ Snek/Snek.csproj | 2 +- Snek/Snek.props | 3 - Snek/Snek.snk | Bin 596 -> 0 bytes Snek/Snek.targets | 61 ++++++ Snek/StrongNameKeyGenerator.cs | 279 ++++++++++++++++++++++++++ 9 files changed, 425 insertions(+), 39 deletions(-) delete mode 100644 .github/workflows/snk.yml create mode 100644 Snek/GenerateStrongNameKeyPairTask.cs delete mode 100644 Snek/Snek.snk create mode 100644 Snek/Snek.targets create mode 100644 Snek/StrongNameKeyGenerator.cs diff --git a/.github/workflows/snk.yml b/.github/workflows/snk.yml deleted file mode 100644 index 6739800..0000000 --- a/.github/workflows/snk.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: snk - -on: - workflow_dispatch: - -jobs: - key: - runs-on: windows-latest - steps: - - name: Locate sn.exe - id: locate-sn - shell: pwsh - run: | - $path = Get-ChildItem -Path "C:\Program Files (x86)\Microsoft SDKs\Windows" -Filter "sn.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - - if (-not $path) { - throw "Could not locate sn.exe" - } - - Write-Output "path=$path" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -NoNewline - - - name: Generate key pair - shell: pwsh - run: | - & "${{ steps.locate-sn.outputs.path }}" -k keys.snk - - - name: Upload the key pair - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: keys - path: keys.snk diff --git a/Readme.md b/Readme.md index d404798..65fff65 100644 --- a/Readme.md +++ b/Readme.md @@ -38,7 +38,7 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me ## Usage -Simply install the **Snek** package as a private dependency in your project and it will automatically sign your assembly with a static key pair: +Simply install the **Snek** package as a private dependency in your project and it will automatically sign your assembly with a strong name key pair, deterministically derived from your package ID (or assembly name, if the package ID is not set): ```xml @@ -49,12 +49,20 @@ Simply install the **Snek** package as a private dependency in your project and You can then inspect the output assembly in **ILSpy** or a similar tool and see that it has a public key token: ```csharp -// Snek.Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=337a390ad141274e +// Snek.Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=929e72f1096e8eac ``` > [!NOTE] -> The public key token will be the same for all assemblies signed with **Snek**. -> For the majority of use cases involving strong naming in modern .NET, this is sufficient. +> The public key token is derived from the seed, so it stays the same across rebuilds and machines, but differs between projects with different package IDs or assembly names. +> You can also set the seed explicitly, to control the resulting public key token independently of the package ID or assembly name: +> +> ```xml +> +> my-custom-seed +> +> ``` +> +> Note that the seed only affects the identity of the resulting public key token — it is not a secret, and the private key is never meant to be kept confidential (it's derived deterministically and can trivially be reproduced by anyone who knows the seed). ## Etymology diff --git a/Snek.Demo/Snek.Demo.csproj b/Snek.Demo/Snek.Demo.csproj index d60a573..307a6b0 100644 --- a/Snek.Demo/Snek.Demo.csproj +++ b/Snek.Demo/Snek.Demo.csproj @@ -15,4 +15,13 @@ + + + + + + + $(MSBuildThisFileDirectory)../Snek/bin/$(Configuration)/ + + diff --git a/Snek/GenerateStrongNameKeyPairTask.cs b/Snek/GenerateStrongNameKeyPairTask.cs new file mode 100644 index 0000000..3c8f551 --- /dev/null +++ b/Snek/GenerateStrongNameKeyPairTask.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Snek; + +public class GenerateStrongNameKeyPairTask : Task +{ + [Required] + public string Seed { get; set; } = ""; + + [Required] + public string CacheDirectory { get; set; } = ""; + + [Output] + public string KeyFilePath { get; set; } = ""; + + public override bool Execute() + { + string seedHash; + using (var sha256 = SHA256.Create()) + seedHash = Convert.ToHexString( + sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Seed)) + ); + + var keyFilePath = Path.Combine(CacheDirectory, seedHash + ".snk"); + + if (!File.Exists(keyFilePath)) + { + Directory.CreateDirectory(CacheDirectory); + + var keyPair = StrongNameKeyGenerator.Generate(Seed); + + // Write to a unique temp file and move it into place atomically, to tolerate + // concurrent inner builds (e.g. multi-targeting) racing to produce the same file. + var tempFilePath = keyFilePath + "." + Guid.NewGuid().ToString("N") + ".tmp"; + try + { + File.WriteAllBytes(tempFilePath, keyPair); + + try + { + File.Move(tempFilePath, keyFilePath); + } + catch (IOException) when (File.Exists(keyFilePath)) + { + // Another concurrent build already produced the file; that's fine. + } + } + finally + { + if (File.Exists(tempFilePath)) + File.Delete(tempFilePath); + } + } + + KeyFilePath = keyFilePath; + + return true; + } +} diff --git a/Snek/Snek.csproj b/Snek/Snek.csproj index 1722a14..9327ea8 100644 --- a/Snek/Snek.csproj +++ b/Snek/Snek.csproj @@ -16,7 +16,7 @@ - + diff --git a/Snek/Snek.props b/Snek/Snek.props index 406c237..73d1537 100644 --- a/Snek/Snek.props +++ b/Snek/Snek.props @@ -2,8 +2,5 @@ true false - $(MSBuildThisFileDirectory)/Snek.snk diff --git a/Snek/Snek.snk b/Snek/Snek.snk deleted file mode 100644 index 42690833286a546b4bb8555b881ce6df794bda46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098G9nhdNk7LOl!ZjjS$c2O09q`ip3_bYA zCxocL{I>0$-n2B8ZVZYa#M$y>UWIXDY>(pbRUl_PeBf^i_ek4S`?;r}4l+aj|In|) zRj>V53co=mKK!*8j79mq>+xgujQlMmv3d1$9dlRX7itDEV&%?dzq)MKy!Y&*c0ari zb<|RJflb?~2b1pC;~AkP`4nr`*K`zmhmhJBrhOPq2(C4Qx{$DZiy!gz( zVeQe(XiD|I&J5YNkZAQ#x{u7tHR%sVz>V|gKaLsue2@b40C6mZn=h&whl6eYSUow% z-#zCpFyM5!p{~`s!Eko;13^Y&e+S+NmigB7`SYcORskNBCIpbt?(s$B6uy8a zdF`_G9a@_o%hhSlhSo*>br4JXS + + + <_SnekTasksDirectory Condition="'$(SnekTasksDirectory)' == ''" + >$(MSBuildThisFileDirectory)../tasks/ + <_SnekTasksDirectory Condition="'$(SnekTasksDirectory)' != ''" + >$(SnekTasksDirectory) + + + + + + + + + <_SnekSeed Condition="'$(AssemblyOriginatorKeySeed)' != ''" + >$(AssemblyOriginatorKeySeed) + <_SnekSeed Condition="'$(_SnekSeed)' == '' and '$(PackageId)' != ''">$(PackageId) + <_SnekSeed Condition="'$(_SnekSeed)' == '' and '$(AssemblyName)' != ''" + >$(AssemblyName) + <_SnekSeed Condition="'$(_SnekSeed)' == ''">$(MSBuildProjectName) + + + + + + + diff --git a/Snek/StrongNameKeyGenerator.cs b/Snek/StrongNameKeyGenerator.cs new file mode 100644 index 0000000..df8dca4 --- /dev/null +++ b/Snek/StrongNameKeyGenerator.cs @@ -0,0 +1,279 @@ +using System; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; + +namespace Snek; + +// Deterministically generates an RSA key pair from a string seed, such that the same seed +// always produces the same key pair, while different seeds produce different key pairs. +// This algorithm is considered frozen: changing it would silently change the resulting key +// (and thus the public key token) for every existing seed, which would be a breaking change. +internal static class StrongNameKeyGenerator +{ + // Matches the key size used by `sn -k` for the strong name key pairs historically shipped + // with this package. + private const int KeySizeBits = 1024; + + private static readonly int[] SmallPrimes = + { + 2, + 3, + 5, + 7, + 11, + 13, + 17, + 19, + 23, + 29, + 31, + 37, + 41, + 43, + 47, + 53, + 59, + 61, + 67, + 71, + 73, + 79, + 83, + 89, + 97, + }; + + public static byte[] Generate(string seed) + { + var stream = new DeterministicByteStream(seed); + var primeBits = KeySizeBits / 2; + + BigInteger p = GeneratePrime(stream, primeBits); + + BigInteger q; + BigInteger e = 65537; + BigInteger d; + BigInteger n; + while (true) + { + q = GeneratePrime(stream, primeBits); + if (q == p) + continue; + + var (pp, qq) = p > q ? (p, q) : (q, p); + var phi = (pp - 1) * (qq - 1); + + if (BigInteger.GreatestCommonDivisor(e, phi) != 1) + continue; + + p = pp; + q = qq; + n = pp * qq; + d = ModInverse(e, phi); + + break; + } + + var dp = d % (p - 1); + var dq = d % (q - 1); + var qInv = ModInverse(q, p); + + var parameters = new RSAParameters + { + Modulus = ToFixedBigEndianBytes(n, KeySizeBits / 8), + Exponent = ToFixedBigEndianBytes(e, 3), + D = ToFixedBigEndianBytes(d, KeySizeBits / 8), + P = ToFixedBigEndianBytes(p, primeBits / 8), + Q = ToFixedBigEndianBytes(q, primeBits / 8), + DP = ToFixedBigEndianBytes(dp, primeBits / 8), + DQ = ToFixedBigEndianBytes(dq, primeBits / 8), + InverseQ = ToFixedBigEndianBytes(qInv, primeBits / 8), + }; + + using var rsa = new RSACryptoServiceProvider(); + rsa.ImportParameters(parameters); + + return rsa.ExportCspBlob(true); + } + + private static BigInteger ModInverse(BigInteger value, BigInteger modulus) + { + var originalModulus = modulus; + BigInteger y = 0; + BigInteger x = 1; + + var a = value % modulus; + if (a < 0) + a += modulus; + + while (a > 1) + { + var quotient = a / modulus; + + (modulus, a) = (a % modulus, modulus); + (y, x) = (x - quotient * y, y); + } + + if (x < 0) + x += originalModulus; + + return x; + } + + private static BigInteger GeneratePrime(DeterministicByteStream stream, int bits) + { + var byteLength = bits / 8; + + while (true) + { + var bytes = stream.Next(byteLength); + + // Force the two most significant bits so that the product of two such primes + // always has the full expected bit length, and force the least significant bit + // so that the candidate is odd. + bytes[byteLength - 1] |= 0b1100_0000; + bytes[0] |= 0x01; + + var candidate = FromBigEndianUnsigned(bytes); + if (IsProbablyPrime(candidate, stream, byteLength)) + return candidate; + } + } + + private static bool IsProbablyPrime( + BigInteger value, + DeterministicByteStream stream, + int byteLength, + int rounds = 32 + ) + { + if (value < 2) + return false; + + foreach (var smallPrime in SmallPrimes) + { + if (value == smallPrime) + return true; + + if (value % smallPrime == 0) + return false; + } + + var d = value - 1; + var r = 0; + while (d.IsEven) + { + d /= 2; + r++; + } + + for (var i = 0; i < rounds; i++) + { + BigInteger witness; + while (true) + { + var candidate = FromBigEndianUnsigned(stream.Next(byteLength)) % (value - 3) + 2; + if (candidate >= 2 && candidate <= value - 2) + { + witness = candidate; + break; + } + } + + var x = BigInteger.ModPow(witness, d, value); + if (x == 1 || x == value - 1) + continue; + + var isComposite = true; + for (var j = 0; j < r - 1; j++) + { + x = BigInteger.ModPow(x, 2, value); + if (x == value - 1) + { + isComposite = false; + break; + } + } + + if (isComposite) + return false; + } + + return true; + } + + // Interprets the provided bytes (in big-endian order) as a non-negative BigInteger. + private static BigInteger FromBigEndianUnsigned(byte[] bigEndianBytes) + { + // BigInteger's byte array constructor expects little-endian, two's complement input. + // An extra trailing zero byte guarantees the value is interpreted as non-negative. + var littleEndianBytes = new byte[bigEndianBytes.Length + 1]; + for (var i = 0; i < bigEndianBytes.Length; i++) + littleEndianBytes[i] = bigEndianBytes[bigEndianBytes.Length - 1 - i]; + + return new BigInteger(littleEndianBytes); + } + + // Converts a non-negative BigInteger to a fixed-length, big-endian, unsigned byte array, + // as required by `RSAParameters`. + private static byte[] ToFixedBigEndianBytes(BigInteger value, int length) + { + var littleEndianBytes = value.ToByteArray(); + var result = new byte[length]; + + for (var i = 0; i < littleEndianBytes.Length; i++) + { + // Skip the extra zero sign byte that `BigInteger.ToByteArray()` may append. + if (littleEndianBytes[i] == 0 && i >= length) + continue; + + result[length - 1 - i] = littleEndianBytes[i]; + } + + return result; + } + + private sealed class DeterministicByteStream + { + private readonly byte[] _seed; + private long _counter; + private byte[] _buffer = Array.Empty(); + private int _bufferPosition; + + public DeterministicByteStream(string seed) => _seed = Encoding.UTF8.GetBytes(seed); + + public byte[] Next(int count) + { + var result = new byte[count]; + var resultPosition = 0; + + while (resultPosition < count) + { + if (_bufferPosition >= _buffer.Length) + { + using var sha256 = SHA256.Create(); + var counterBytes = BitConverter.GetBytes(_counter); + + sha256.TransformBlock(_seed, 0, _seed.Length, null, 0); + sha256.TransformFinalBlock(counterBytes, 0, counterBytes.Length); + + _buffer = sha256.Hash!; + _bufferPosition = 0; + _counter++; + } + + var chunkLength = Math.Min( + count - resultPosition, + _buffer.Length - _bufferPosition + ); + Buffer.BlockCopy(_buffer, _bufferPosition, result, resultPosition, chunkLength); + + _bufferPosition += chunkLength; + resultPosition += chunkLength; + } + + return result; + } + } +} From 16705b5cb55de58facd2cadaabe02c219203c16f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:34:14 +0000 Subject: [PATCH 2/9] Address review feedback: rename types, adopt MacBundle-style local props, add error for manual key file, simplify Readme Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- Directory.Packages.props | 2 +- Readme.md | 23 ++++--- Snek.Demo/Snek.Demo.csproj | 7 +- Snek/GenerateStrongNameKeyPairTask.cs | 63 ------------------ Snek/Snek.Local.props | 17 +++++ Snek/Snek.props | 17 ++++- Snek/Snek.targets | 65 ++++++------------- ...meKeyGenerator.cs => StrongNameKeyPair.cs} | 16 +++-- Snek/StrongNameTask.cs | 61 +++++++++++++++++ 9 files changed, 139 insertions(+), 132 deletions(-) delete mode 100644 Snek/GenerateStrongNameKeyPairTask.cs create mode 100644 Snek/Snek.Local.props rename Snek/{StrongNameKeyGenerator.cs => StrongNameKeyPair.cs} (89%) create mode 100644 Snek/StrongNameTask.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index d5e64ab..7743556 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ - + \ No newline at end of file diff --git a/Readme.md b/Readme.md index 65fff65..f35dae5 100644 --- a/Readme.md +++ b/Readme.md @@ -38,7 +38,7 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me ## Usage -Simply install the **Snek** package as a private dependency in your project and it will automatically sign your assembly with a strong name key pair, deterministically derived from your package ID (or assembly name, if the package ID is not set): +Simply install the **Snek** package as a private dependency in your project and it will automatically sign your assembly with a strong name key pair, deterministically derived from your package ID or assembly name (whichever is set): ```xml @@ -52,17 +52,16 @@ You can then inspect the output assembly in **ILSpy** or a similar tool and see // Snek.Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=929e72f1096e8eac ``` -> [!NOTE] -> The public key token is derived from the seed, so it stays the same across rebuilds and machines, but differs between projects with different package IDs or assembly names. -> You can also set the seed explicitly, to control the resulting public key token independently of the package ID or assembly name: -> -> ```xml -> -> my-custom-seed -> -> ``` -> -> Note that the seed only affects the identity of the resulting public key token — it is not a secret, and the private key is never meant to be kept confidential (it's derived deterministically and can trivially be reproduced by anyone who knows the seed). +The public key token is derived from the seed, so it stays the same across rebuilds and machines, but differs between projects with different package IDs or assembly names. +You can also set the seed explicitly, to control the resulting public key token independently of the package ID or assembly name: + +```xml + + my-custom-seed + +``` + +Note that the seed only affects the identity of the resulting public key token — it is not a secret, and the private key is never meant to be kept confidential (it's derived deterministically and can trivially be reproduced by anyone who knows the seed). ## Etymology diff --git a/Snek.Demo/Snek.Demo.csproj b/Snek.Demo/Snek.Demo.csproj index 307a6b0..cff9a14 100644 --- a/Snek.Demo/Snek.Demo.csproj +++ b/Snek.Demo/Snek.Demo.csproj @@ -1,7 +1,7 @@ - + Exe @@ -18,10 +18,5 @@ - - - - $(MSBuildThisFileDirectory)../Snek/bin/$(Configuration)/ - diff --git a/Snek/GenerateStrongNameKeyPairTask.cs b/Snek/GenerateStrongNameKeyPairTask.cs deleted file mode 100644 index 3c8f551..0000000 --- a/Snek/GenerateStrongNameKeyPairTask.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace Snek; - -public class GenerateStrongNameKeyPairTask : Task -{ - [Required] - public string Seed { get; set; } = ""; - - [Required] - public string CacheDirectory { get; set; } = ""; - - [Output] - public string KeyFilePath { get; set; } = ""; - - public override bool Execute() - { - string seedHash; - using (var sha256 = SHA256.Create()) - seedHash = Convert.ToHexString( - sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Seed)) - ); - - var keyFilePath = Path.Combine(CacheDirectory, seedHash + ".snk"); - - if (!File.Exists(keyFilePath)) - { - Directory.CreateDirectory(CacheDirectory); - - var keyPair = StrongNameKeyGenerator.Generate(Seed); - - // Write to a unique temp file and move it into place atomically, to tolerate - // concurrent inner builds (e.g. multi-targeting) racing to produce the same file. - var tempFilePath = keyFilePath + "." + Guid.NewGuid().ToString("N") + ".tmp"; - try - { - File.WriteAllBytes(tempFilePath, keyPair); - - try - { - File.Move(tempFilePath, keyFilePath); - } - catch (IOException) when (File.Exists(keyFilePath)) - { - // Another concurrent build already produced the file; that's fine. - } - } - finally - { - if (File.Exists(tempFilePath)) - File.Delete(tempFilePath); - } - } - - KeyFilePath = keyFilePath; - - return true; - } -} diff --git a/Snek/Snek.Local.props b/Snek/Snek.Local.props new file mode 100644 index 0000000..eca65ef --- /dev/null +++ b/Snek/Snek.Local.props @@ -0,0 +1,17 @@ + + + + + + $(MSBuildThisFileDirectory)/bin/$(Configuration)/netstandard2.0/Snek.dll + $(MSBuildThisFileDirectory)/bin/$(Configuration)/net472/Snek.dll + + diff --git a/Snek/Snek.props b/Snek/Snek.props index 73d1537..9662cbb 100644 --- a/Snek/Snek.props +++ b/Snek/Snek.props @@ -1,6 +1,21 @@ - + true false + + + $(MSBuildThisFileDirectory)/../tasks/netstandard2.0/Snek.dll + $(MSBuildThisFileDirectory)/../tasks/net472/Snek.dll + + + diff --git a/Snek/Snek.targets b/Snek/Snek.targets index 02d7821..b65c7ce 100644 --- a/Snek/Snek.targets +++ b/Snek/Snek.targets @@ -1,61 +1,38 @@ - - - <_SnekTasksDirectory Condition="'$(SnekTasksDirectory)' == ''" - >$(MSBuildThisFileDirectory)../tasks/ - <_SnekTasksDirectory Condition="'$(SnekTasksDirectory)' != ''" - >$(SnekTasksDirectory) - - - - - + + - <_SnekSeed Condition="'$(AssemblyOriginatorKeySeed)' != ''" - >$(AssemblyOriginatorKeySeed)$(PackageId) + $(AssemblyName) - <_SnekSeed Condition="'$(_SnekSeed)' == '' and '$(PackageId)' != ''">$(PackageId) - <_SnekSeed Condition="'$(_SnekSeed)' == '' and '$(AssemblyName)' != ''" - >$(AssemblyName)$(MSBuildProjectName) - <_SnekSeed Condition="'$(_SnekSeed)' == ''">$(MSBuildProjectName) - - + diff --git a/Snek/StrongNameKeyGenerator.cs b/Snek/StrongNameKeyPair.cs similarity index 89% rename from Snek/StrongNameKeyGenerator.cs rename to Snek/StrongNameKeyPair.cs index df8dca4..f4644a0 100644 --- a/Snek/StrongNameKeyGenerator.cs +++ b/Snek/StrongNameKeyPair.cs @@ -9,7 +9,7 @@ namespace Snek; // always produces the same key pair, while different seeds produce different key pairs. // This algorithm is considered frozen: changing it would silently change the resulting key // (and thus the public key token) for every existing seed, which would be a breaking change. -internal static class StrongNameKeyGenerator +internal static class StrongNameKeyPair { // Matches the key size used by `sn -k` for the strong name key pairs historically shipped // with this package. @@ -234,6 +234,12 @@ private static byte[] ToFixedBigEndianBytes(BigInteger value, int length) return result; } + // A deterministic pseudo-random byte generator, seeded from the provided string. + // Used in place of a true RNG so that prime search and Miller-Rabin witness selection + // (which both consume an arbitrary, a priori unknown amount of random-looking data) + // produce the exact same sequence of bytes for the same seed on every run. + // Internally, it hashes the seed together with an incrementing counter to produce an + // unbounded stream of pseudo-random bytes (a simple counter-mode hash construction). private sealed class DeterministicByteStream { private readonly byte[] _seed; @@ -252,13 +258,13 @@ public byte[] Next(int count) { if (_bufferPosition >= _buffer.Length) { - using var sha256 = SHA256.Create(); var counterBytes = BitConverter.GetBytes(_counter); + var input = new byte[_seed.Length + counterBytes.Length]; - sha256.TransformBlock(_seed, 0, _seed.Length, null, 0); - sha256.TransformFinalBlock(counterBytes, 0, counterBytes.Length); + Buffer.BlockCopy(_seed, 0, input, 0, _seed.Length); + Buffer.BlockCopy(counterBytes, 0, input, _seed.Length, counterBytes.Length); - _buffer = sha256.Hash!; + _buffer = SHA256.HashData(input); _bufferPosition = 0; _counter++; } diff --git a/Snek/StrongNameTask.cs b/Snek/StrongNameTask.cs new file mode 100644 index 0000000..949be9c --- /dev/null +++ b/Snek/StrongNameTask.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Snek; + +public class StrongNameTask : Task +{ + [Required] + public string Seed { get; set; } = ""; + + [Required] + public string CacheDirectoryPath { get; set; } = ""; + + [Output] + public string KeyFilePath { get; set; } = ""; + + public override bool Execute() + { + var seedHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(Seed))); + var keyFilePath = Path.Combine(CacheDirectoryPath, seedHash + ".snk"); + + if (File.Exists(keyFilePath)) + { + KeyFilePath = keyFilePath; + return true; + } + + Directory.CreateDirectory(CacheDirectoryPath); + + var keyPair = StrongNameKeyPair.Generate(Seed); + + // Write to a unique temp file and move it into place atomically, to tolerate + // concurrent inner builds (e.g. multi-targeting) racing to produce the same file. + var tempFilePath = keyFilePath + "." + Guid.NewGuid().ToString("N") + ".tmp"; + try + { + File.WriteAllBytes(tempFilePath, keyPair); + + try + { + File.Move(tempFilePath, keyFilePath); + } + catch (IOException) when (File.Exists(keyFilePath)) + { + // Another concurrent build already produced the file; that's fine. + } + } + finally + { + File.Delete(tempFilePath); + } + + KeyFilePath = keyFilePath; + + return true; + } +} From a8d3243f69ab6cb0f1eb9e4ca90c03cb0f1d4770 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:49:12 +0000 Subject: [PATCH 3/9] Address second round of review feedback: simplify target condition, reorder demo imports, Readme tweaks Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- Readme.md | 4 +--- Snek.Demo/Snek.Demo.csproj | 6 +----- Snek/Snek.targets | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Readme.md b/Readme.md index f35dae5..40a5282 100644 --- a/Readme.md +++ b/Readme.md @@ -52,7 +52,7 @@ You can then inspect the output assembly in **ILSpy** or a similar tool and see // Snek.Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=929e72f1096e8eac ``` -The public key token is derived from the seed, so it stays the same across rebuilds and machines, but differs between projects with different package IDs or assembly names. +The public key token is derived from a seed, so it stays the same across rebuilds and machines, but differs between projects with different package IDs or assembly names. You can also set the seed explicitly, to control the resulting public key token independently of the package ID or assembly name: ```xml @@ -61,8 +61,6 @@ You can also set the seed explicitly, to control the resulting public key token ``` -Note that the seed only affects the identity of the resulting public key token — it is not a secret, and the private key is never meant to be kept confidential (it's derived deterministically and can trivially be reproduced by anyone who knows the seed). - ## Etymology **Snek** is a playful nickname for the `sn -k` command, which is used to generate a strong name key pair. diff --git a/Snek.Demo/Snek.Demo.csproj b/Snek.Demo/Snek.Demo.csproj index cff9a14..95fb81e 100644 --- a/Snek.Demo/Snek.Demo.csproj +++ b/Snek.Demo/Snek.Demo.csproj @@ -1,7 +1,7 @@ - + Exe @@ -15,8 +15,4 @@ - - - - diff --git a/Snek/Snek.targets b/Snek/Snek.targets index b65c7ce..b807f17 100644 --- a/Snek/Snek.targets +++ b/Snek/Snek.targets @@ -7,7 +7,7 @@ From b6134a247959d59a359d0907c62eebbc37bd39cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:56:17 +0000 Subject: [PATCH 4/9] Extract DeterministicByteStream into a standalone SeededRandom utility type Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- Snek/SeededRandom.cs | 51 +++++++++++++++++++++++++++++ Snek/StrongNameKeyPair.cs | 67 ++++++--------------------------------- 2 files changed, 60 insertions(+), 58 deletions(-) create mode 100644 Snek/SeededRandom.cs diff --git a/Snek/SeededRandom.cs b/Snek/SeededRandom.cs new file mode 100644 index 0000000..719a41e --- /dev/null +++ b/Snek/SeededRandom.cs @@ -0,0 +1,51 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Snek; + +// A deterministic pseudo-random byte generator, seeded from the provided string. +// Unlike `System.Random`, whose seeded sequence is not guaranteed to be stable across +// runtimes or versions, this type is based on SHA256, which produces the exact same +// sequence of bytes for the same seed on every run, on every target framework. +// Internally, it hashes the seed together with an incrementing counter to produce an +// unbounded stream of pseudo-random bytes (a simple counter-mode hash construction). +internal sealed class SeededRandom +{ + private readonly byte[] _seed; + private long _counter; + private byte[] _buffer = Array.Empty(); + private int _bufferPosition; + + public SeededRandom(string seed) => _seed = Encoding.UTF8.GetBytes(seed); + + public byte[] NextBytes(int count) + { + var result = new byte[count]; + var resultPosition = 0; + + while (resultPosition < count) + { + if (_bufferPosition >= _buffer.Length) + { + var counterBytes = BitConverter.GetBytes(_counter); + var input = new byte[_seed.Length + counterBytes.Length]; + + Buffer.BlockCopy(_seed, 0, input, 0, _seed.Length); + Buffer.BlockCopy(counterBytes, 0, input, _seed.Length, counterBytes.Length); + + _buffer = SHA256.HashData(input); + _bufferPosition = 0; + _counter++; + } + + var chunkLength = Math.Min(count - resultPosition, _buffer.Length - _bufferPosition); + Buffer.BlockCopy(_buffer, _bufferPosition, result, resultPosition, chunkLength); + + _bufferPosition += chunkLength; + resultPosition += chunkLength; + } + + return result; + } +} diff --git a/Snek/StrongNameKeyPair.cs b/Snek/StrongNameKeyPair.cs index f4644a0..70604f3 100644 --- a/Snek/StrongNameKeyPair.cs +++ b/Snek/StrongNameKeyPair.cs @@ -1,7 +1,6 @@ using System; using System.Numerics; using System.Security.Cryptography; -using System.Text; namespace Snek; @@ -46,10 +45,10 @@ internal static class StrongNameKeyPair public static byte[] Generate(string seed) { - var stream = new DeterministicByteStream(seed); + var random = new SeededRandom(seed); var primeBits = KeySizeBits / 2; - BigInteger p = GeneratePrime(stream, primeBits); + BigInteger p = GeneratePrime(random, primeBits); BigInteger q; BigInteger e = 65537; @@ -57,7 +56,7 @@ public static byte[] Generate(string seed) BigInteger n; while (true) { - q = GeneratePrime(stream, primeBits); + q = GeneratePrime(random, primeBits); if (q == p) continue; @@ -121,13 +120,13 @@ private static BigInteger ModInverse(BigInteger value, BigInteger modulus) return x; } - private static BigInteger GeneratePrime(DeterministicByteStream stream, int bits) + private static BigInteger GeneratePrime(SeededRandom random, int bits) { var byteLength = bits / 8; while (true) { - var bytes = stream.Next(byteLength); + var bytes = random.NextBytes(byteLength); // Force the two most significant bits so that the product of two such primes // always has the full expected bit length, and force the least significant bit @@ -136,14 +135,14 @@ private static BigInteger GeneratePrime(DeterministicByteStream stream, int bits bytes[0] |= 0x01; var candidate = FromBigEndianUnsigned(bytes); - if (IsProbablyPrime(candidate, stream, byteLength)) + if (IsProbablyPrime(candidate, random, byteLength)) return candidate; } } private static bool IsProbablyPrime( BigInteger value, - DeterministicByteStream stream, + SeededRandom random, int byteLength, int rounds = 32 ) @@ -173,7 +172,8 @@ private static bool IsProbablyPrime( BigInteger witness; while (true) { - var candidate = FromBigEndianUnsigned(stream.Next(byteLength)) % (value - 3) + 2; + var candidate = + FromBigEndianUnsigned(random.NextBytes(byteLength)) % (value - 3) + 2; if (candidate >= 2 && candidate <= value - 2) { witness = candidate; @@ -233,53 +233,4 @@ private static byte[] ToFixedBigEndianBytes(BigInteger value, int length) return result; } - - // A deterministic pseudo-random byte generator, seeded from the provided string. - // Used in place of a true RNG so that prime search and Miller-Rabin witness selection - // (which both consume an arbitrary, a priori unknown amount of random-looking data) - // produce the exact same sequence of bytes for the same seed on every run. - // Internally, it hashes the seed together with an incrementing counter to produce an - // unbounded stream of pseudo-random bytes (a simple counter-mode hash construction). - private sealed class DeterministicByteStream - { - private readonly byte[] _seed; - private long _counter; - private byte[] _buffer = Array.Empty(); - private int _bufferPosition; - - public DeterministicByteStream(string seed) => _seed = Encoding.UTF8.GetBytes(seed); - - public byte[] Next(int count) - { - var result = new byte[count]; - var resultPosition = 0; - - while (resultPosition < count) - { - if (_bufferPosition >= _buffer.Length) - { - var counterBytes = BitConverter.GetBytes(_counter); - var input = new byte[_seed.Length + counterBytes.Length]; - - Buffer.BlockCopy(_seed, 0, input, 0, _seed.Length); - Buffer.BlockCopy(counterBytes, 0, input, _seed.Length, counterBytes.Length); - - _buffer = SHA256.HashData(input); - _bufferPosition = 0; - _counter++; - } - - var chunkLength = Math.Min( - count - resultPosition, - _buffer.Length - _bufferPosition - ); - Buffer.BlockCopy(_buffer, _bufferPosition, result, resultPosition, chunkLength); - - _bufferPosition += chunkLength; - resultPosition += chunkLength; - } - - return result; - } - } } From d58efd740515922ed6a602d4f2269bc0db074634 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:10:41 +0000 Subject: [PATCH 5/9] Address review feedback: move utils to Utils namespace, use primary constructors/collection expressions, remove unused package, reorder members Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- Directory.Packages.props | 1 - Snek/StrongNameTask.cs | 1 + Snek/{ => Utils}/SeededRandom.cs | 10 +-- Snek/{ => Utils}/StrongNameKeyPair.cs | 108 +++++++++++++------------- 4 files changed, 59 insertions(+), 61 deletions(-) rename Snek/{ => Utils}/SeededRandom.cs (88%) rename Snek/{ => Utils}/StrongNameKeyPair.cs (98%) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7743556..4fd73b9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,5 @@ - \ No newline at end of file diff --git a/Snek/StrongNameTask.cs b/Snek/StrongNameTask.cs index 949be9c..c2adbad 100644 --- a/Snek/StrongNameTask.cs +++ b/Snek/StrongNameTask.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Snek.Utils; namespace Snek; diff --git a/Snek/SeededRandom.cs b/Snek/Utils/SeededRandom.cs similarity index 88% rename from Snek/SeededRandom.cs rename to Snek/Utils/SeededRandom.cs index 719a41e..960e5dc 100644 --- a/Snek/SeededRandom.cs +++ b/Snek/Utils/SeededRandom.cs @@ -2,7 +2,7 @@ using System.Security.Cryptography; using System.Text; -namespace Snek; +namespace Snek.Utils; // A deterministic pseudo-random byte generator, seeded from the provided string. // Unlike `System.Random`, whose seeded sequence is not guaranteed to be stable across @@ -10,15 +10,13 @@ namespace Snek; // sequence of bytes for the same seed on every run, on every target framework. // Internally, it hashes the seed together with an incrementing counter to produce an // unbounded stream of pseudo-random bytes (a simple counter-mode hash construction). -internal sealed class SeededRandom +internal class SeededRandom(string seed) { - private readonly byte[] _seed; + private readonly byte[] _seed = Encoding.UTF8.GetBytes(seed); private long _counter; - private byte[] _buffer = Array.Empty(); + private byte[] _buffer = []; private int _bufferPosition; - public SeededRandom(string seed) => _seed = Encoding.UTF8.GetBytes(seed); - public byte[] NextBytes(int count) { var result = new byte[count]; diff --git a/Snek/StrongNameKeyPair.cs b/Snek/Utils/StrongNameKeyPair.cs similarity index 98% rename from Snek/StrongNameKeyPair.cs rename to Snek/Utils/StrongNameKeyPair.cs index 70604f3..4026217 100644 --- a/Snek/StrongNameKeyPair.cs +++ b/Snek/Utils/StrongNameKeyPair.cs @@ -2,7 +2,7 @@ using System.Numerics; using System.Security.Cryptography; -namespace Snek; +namespace Snek.Utils; // Deterministically generates an RSA key pair from a string seed, such that the same seed // always produces the same key pair, while different seeds produce different key pairs. @@ -43,59 +43,6 @@ internal static class StrongNameKeyPair 97, }; - public static byte[] Generate(string seed) - { - var random = new SeededRandom(seed); - var primeBits = KeySizeBits / 2; - - BigInteger p = GeneratePrime(random, primeBits); - - BigInteger q; - BigInteger e = 65537; - BigInteger d; - BigInteger n; - while (true) - { - q = GeneratePrime(random, primeBits); - if (q == p) - continue; - - var (pp, qq) = p > q ? (p, q) : (q, p); - var phi = (pp - 1) * (qq - 1); - - if (BigInteger.GreatestCommonDivisor(e, phi) != 1) - continue; - - p = pp; - q = qq; - n = pp * qq; - d = ModInverse(e, phi); - - break; - } - - var dp = d % (p - 1); - var dq = d % (q - 1); - var qInv = ModInverse(q, p); - - var parameters = new RSAParameters - { - Modulus = ToFixedBigEndianBytes(n, KeySizeBits / 8), - Exponent = ToFixedBigEndianBytes(e, 3), - D = ToFixedBigEndianBytes(d, KeySizeBits / 8), - P = ToFixedBigEndianBytes(p, primeBits / 8), - Q = ToFixedBigEndianBytes(q, primeBits / 8), - DP = ToFixedBigEndianBytes(dp, primeBits / 8), - DQ = ToFixedBigEndianBytes(dq, primeBits / 8), - InverseQ = ToFixedBigEndianBytes(qInv, primeBits / 8), - }; - - using var rsa = new RSACryptoServiceProvider(); - rsa.ImportParameters(parameters); - - return rsa.ExportCspBlob(true); - } - private static BigInteger ModInverse(BigInteger value, BigInteger modulus) { var originalModulus = modulus; @@ -233,4 +180,57 @@ private static byte[] ToFixedBigEndianBytes(BigInteger value, int length) return result; } + + public static byte[] Generate(string seed) + { + var random = new SeededRandom(seed); + var primeBits = KeySizeBits / 2; + + var p = GeneratePrime(random, primeBits); + + BigInteger q; + BigInteger e = 65537; + BigInteger d; + BigInteger n; + while (true) + { + q = GeneratePrime(random, primeBits); + if (q == p) + continue; + + var (pp, qq) = p > q ? (p, q) : (q, p); + var phi = (pp - 1) * (qq - 1); + + if (BigInteger.GreatestCommonDivisor(e, phi) != 1) + continue; + + p = pp; + q = qq; + n = pp * qq; + d = ModInverse(e, phi); + + break; + } + + var dp = d % (p - 1); + var dq = d % (q - 1); + var qInv = ModInverse(q, p); + + var parameters = new RSAParameters + { + Modulus = ToFixedBigEndianBytes(n, KeySizeBits / 8), + Exponent = ToFixedBigEndianBytes(e, 3), + D = ToFixedBigEndianBytes(d, KeySizeBits / 8), + P = ToFixedBigEndianBytes(p, primeBits / 8), + Q = ToFixedBigEndianBytes(q, primeBits / 8), + DP = ToFixedBigEndianBytes(dp, primeBits / 8), + DQ = ToFixedBigEndianBytes(dq, primeBits / 8), + InverseQ = ToFixedBigEndianBytes(qInv, primeBits / 8), + }; + + using var rsa = new RSACryptoServiceProvider(); + rsa.ImportParameters(parameters); + + return rsa.ExportCspBlob(true); + } } From 965ce5a9956c9207f5ba2e5e99913dd10429ebe6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:28:49 +0000 Subject: [PATCH 6/9] Fix strong-name key generation byte order Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- Snek/Utils/SeededRandom.cs | 4 +++- Snek/Utils/StrongNameKeyPair.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Snek/Utils/SeededRandom.cs b/Snek/Utils/SeededRandom.cs index 960e5dc..c11cb90 100644 --- a/Snek/Utils/SeededRandom.cs +++ b/Snek/Utils/SeededRandom.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; @@ -26,7 +27,8 @@ public byte[] NextBytes(int count) { if (_bufferPosition >= _buffer.Length) { - var counterBytes = BitConverter.GetBytes(_counter); + var counterBytes = new byte[sizeof(long)]; + BinaryPrimitives.WriteInt64BigEndian(counterBytes, _counter); var input = new byte[_seed.Length + counterBytes.Length]; Buffer.BlockCopy(_seed, 0, input, 0, _seed.Length); diff --git a/Snek/Utils/StrongNameKeyPair.cs b/Snek/Utils/StrongNameKeyPair.cs index 4026217..e0eb0a4 100644 --- a/Snek/Utils/StrongNameKeyPair.cs +++ b/Snek/Utils/StrongNameKeyPair.cs @@ -78,8 +78,8 @@ private static BigInteger GeneratePrime(SeededRandom random, int bits) // Force the two most significant bits so that the product of two such primes // always has the full expected bit length, and force the least significant bit // so that the candidate is odd. - bytes[byteLength - 1] |= 0b1100_0000; - bytes[0] |= 0x01; + bytes[0] |= 0b1100_0000; + bytes[byteLength - 1] |= 0x01; var candidate = FromBigEndianUnsigned(bytes); if (IsProbablyPrime(candidate, random, byteLength)) From c50161fa59ff8cfcdff984b56762b88784ce1b7f Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:35:41 +0300 Subject: [PATCH 7/9] Update StrongNameTask.cs --- Snek/StrongNameTask.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Snek/StrongNameTask.cs b/Snek/StrongNameTask.cs index c2adbad..0db0971 100644 --- a/Snek/StrongNameTask.cs +++ b/Snek/StrongNameTask.cs @@ -11,13 +11,13 @@ namespace Snek; public class StrongNameTask : Task { [Required] - public string Seed { get; set; } = ""; + public required string Seed { get; set; } [Required] - public string CacheDirectoryPath { get; set; } = ""; + public required string CacheDirectoryPath { get; set; } [Output] - public string KeyFilePath { get; set; } = ""; + public string? KeyFilePath { get; set; } public override bool Execute() { From e33082801277caf68f798bd77b2603ab0ef48234 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:27:34 +0000 Subject: [PATCH 8/9] Use BigInteger byte-array polyfill Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- Directory.Packages.props | 2 +- Snek/Utils/Extensions/BigIntegerExtensions.cs | 18 ++++++++++ Snek/Utils/StrongNameKeyPair.cs | 35 ++++++------------- 3 files changed, 30 insertions(+), 25 deletions(-) create mode 100644 Snek/Utils/Extensions/BigIntegerExtensions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 4fd73b9..d7cefb3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,6 @@ - + \ No newline at end of file diff --git a/Snek/Utils/Extensions/BigIntegerExtensions.cs b/Snek/Utils/Extensions/BigIntegerExtensions.cs new file mode 100644 index 0000000..08ae754 --- /dev/null +++ b/Snek/Utils/Extensions/BigIntegerExtensions.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Snek.Utils.Extensions; + +internal static class BigIntegerExtensions +{ + // Interprets the provided bytes (in big-endian order) as a non-negative BigInteger. + public static BigInteger FromBigEndianUnsigned(this byte[] bigEndianBytes) + { + // BigInteger's byte array constructor expects little-endian, two's complement input. + // An extra trailing zero byte guarantees the value is interpreted as non-negative. + var littleEndianBytes = new byte[bigEndianBytes.Length + 1]; + for (var i = 0; i < bigEndianBytes.Length; i++) + littleEndianBytes[i] = bigEndianBytes[bigEndianBytes.Length - 1 - i]; + + return new BigInteger(littleEndianBytes); + } +} diff --git a/Snek/Utils/StrongNameKeyPair.cs b/Snek/Utils/StrongNameKeyPair.cs index e0eb0a4..ea0c960 100644 --- a/Snek/Utils/StrongNameKeyPair.cs +++ b/Snek/Utils/StrongNameKeyPair.cs @@ -1,6 +1,7 @@ using System; using System.Numerics; using System.Security.Cryptography; +using Snek.Utils.Extensions; namespace Snek.Utils; @@ -81,7 +82,7 @@ private static BigInteger GeneratePrime(SeededRandom random, int bits) bytes[0] |= 0b1100_0000; bytes[byteLength - 1] |= 0x01; - var candidate = FromBigEndianUnsigned(bytes); + var candidate = bytes.FromBigEndianUnsigned(); if (IsProbablyPrime(candidate, random, byteLength)) return candidate; } @@ -120,7 +121,7 @@ private static bool IsProbablyPrime( while (true) { var candidate = - FromBigEndianUnsigned(random.NextBytes(byteLength)) % (value - 3) + 2; + random.NextBytes(byteLength).FromBigEndianUnsigned() % (value - 3) + 2; if (candidate >= 2 && candidate <= value - 2) { witness = candidate; @@ -150,33 +151,19 @@ private static bool IsProbablyPrime( return true; } - // Interprets the provided bytes (in big-endian order) as a non-negative BigInteger. - private static BigInteger FromBigEndianUnsigned(byte[] bigEndianBytes) - { - // BigInteger's byte array constructor expects little-endian, two's complement input. - // An extra trailing zero byte guarantees the value is interpreted as non-negative. - var littleEndianBytes = new byte[bigEndianBytes.Length + 1]; - for (var i = 0; i < bigEndianBytes.Length; i++) - littleEndianBytes[i] = bigEndianBytes[bigEndianBytes.Length - 1 - i]; - - return new BigInteger(littleEndianBytes); - } - // Converts a non-negative BigInteger to a fixed-length, big-endian, unsigned byte array, // as required by `RSAParameters`. private static byte[] ToFixedBigEndianBytes(BigInteger value, int length) { - var littleEndianBytes = value.ToByteArray(); + var bigEndianBytes = value.ToByteArray(isUnsigned: true, isBigEndian: true); var result = new byte[length]; - - for (var i = 0; i < littleEndianBytes.Length; i++) - { - // Skip the extra zero sign byte that `BigInteger.ToByteArray()` may append. - if (littleEndianBytes[i] == 0 && i >= length) - continue; - - result[length - 1 - i] = littleEndianBytes[i]; - } + Buffer.BlockCopy( + bigEndianBytes, + 0, + result, + length - bigEndianBytes.Length, + bigEndianBytes.Length + ); return result; } From 15da2a7eca543938720abda807802206aee418ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:31:07 +0000 Subject: [PATCH 9/9] Refactor BigInteger factory extension Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- Snek/Utils/Extensions/BigIntegerExtensions.cs | 19 +++++++++++-------- Snek/Utils/StrongNameKeyPair.cs | 5 +++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Snek/Utils/Extensions/BigIntegerExtensions.cs b/Snek/Utils/Extensions/BigIntegerExtensions.cs index 08ae754..33f6f60 100644 --- a/Snek/Utils/Extensions/BigIntegerExtensions.cs +++ b/Snek/Utils/Extensions/BigIntegerExtensions.cs @@ -4,15 +4,18 @@ namespace Snek.Utils.Extensions; internal static class BigIntegerExtensions { - // Interprets the provided bytes (in big-endian order) as a non-negative BigInteger. - public static BigInteger FromBigEndianUnsigned(this byte[] bigEndianBytes) + extension(BigInteger) { - // BigInteger's byte array constructor expects little-endian, two's complement input. - // An extra trailing zero byte guarantees the value is interpreted as non-negative. - var littleEndianBytes = new byte[bigEndianBytes.Length + 1]; - for (var i = 0; i < bigEndianBytes.Length; i++) - littleEndianBytes[i] = bigEndianBytes[bigEndianBytes.Length - 1 - i]; + // Interprets the provided bytes (in big-endian order) as a non-negative BigInteger. + public static BigInteger FromBigEndianUnsigned(byte[] bigEndianBytes) + { + // BigInteger's byte array constructor expects little-endian, two's complement input. + // An extra trailing zero byte guarantees the value is interpreted as non-negative. + var littleEndianBytes = new byte[bigEndianBytes.Length + 1]; + for (var i = 0; i < bigEndianBytes.Length; i++) + littleEndianBytes[i] = bigEndianBytes[bigEndianBytes.Length - 1 - i]; - return new BigInteger(littleEndianBytes); + return new BigInteger(littleEndianBytes); + } } } diff --git a/Snek/Utils/StrongNameKeyPair.cs b/Snek/Utils/StrongNameKeyPair.cs index ea0c960..d447b63 100644 --- a/Snek/Utils/StrongNameKeyPair.cs +++ b/Snek/Utils/StrongNameKeyPair.cs @@ -82,7 +82,7 @@ private static BigInteger GeneratePrime(SeededRandom random, int bits) bytes[0] |= 0b1100_0000; bytes[byteLength - 1] |= 0x01; - var candidate = bytes.FromBigEndianUnsigned(); + var candidate = BigInteger.FromBigEndianUnsigned(bytes); if (IsProbablyPrime(candidate, random, byteLength)) return candidate; } @@ -121,7 +121,8 @@ private static bool IsProbablyPrime( while (true) { var candidate = - random.NextBytes(byteLength).FromBigEndianUnsigned() % (value - 3) + 2; + BigInteger.FromBigEndianUnsigned(random.NextBytes(byteLength)) % (value - 3) + + 2; if (candidate >= 2 && candidate <= value - 2) { witness = candidate;