diff --git a/.editorconfig b/.editorconfig
index 06fbf7f64..bbd3bbfde 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -275,3 +275,7 @@ dotnet_diagnostic.CA2242.severity = warning
# MEF006: No importing constructor
dotnet_diagnostic.MEF006.severity = silent
+
+# Files vendored from dotnet/runtime keep their upstream formatting so future syncs diff cleanly.
+[ICSharpCode.Decompiler/Util/Sha1ForNonSecretPurposes.cs]
+generated_code = true
diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index 210d34749..b984447c2 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -395,6 +395,7 @@
+
diff --git a/ICSharpCode.Decompiler.Tests/Util/Sha1ForNonSecretPurposesTests.cs b/ICSharpCode.Decompiler.Tests/Util/Sha1ForNonSecretPurposesTests.cs
new file mode 100644
index 000000000..9f3f2df7e
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/Util/Sha1ForNonSecretPurposesTests.cs
@@ -0,0 +1,111 @@
+// Copyright (c) 2026 Siegfried Pammer
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.Decompiler.Tests.Util
+{
+ [TestFixture]
+ public class Sha1ForNonSecretPurposesTests
+ {
+ static string OneShotHash(byte[] input)
+ {
+ byte[] output = new byte[20];
+ Sha1ForNonSecretPurposes.HashData(input, output);
+ return ToHex(output);
+ }
+
+ static string IncrementalHash(byte[] input)
+ {
+ var sha1 = default(Sha1ForNonSecretPurposes);
+ sha1.Start();
+ sha1.Append(input);
+ byte[] output = new byte[20];
+ sha1.Finish(output);
+ return ToHex(output);
+ }
+
+ static string ToHex(byte[] bytes)
+ {
+ return string.Concat(bytes.Select(b => b.ToString("x2")));
+ }
+
+ // FIPS 180 test vectors
+ [TestCase("", "da39a3ee5e6b4b0d3255bfef95601890afd80709")]
+ [TestCase("abc", "a9993e364706816aba3e25717850c26c9cd0d89d")]
+ [TestCase("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "84983e441c3bd26ebaae4aa1f95129e5e54670f1")]
+ public void KnownVectors(string input, string expectedHex)
+ {
+ byte[] bytes = Encoding.ASCII.GetBytes(input);
+ Assert.That(OneShotHash(bytes), Is.EqualTo(expectedHex));
+ Assert.That(IncrementalHash(bytes), Is.EqualTo(expectedHex));
+ }
+
+ [Test]
+ public void MatchesPlatformSha1AroundBlockBoundaries()
+ {
+ // Cover all interesting positions relative to the 64-byte block size and the
+ // 8-byte length suffix: 0, 1, 55, 56, 57, 63, 64, 65, 119, 120, 127, 128, ...
+ using var platformSha1 = System.Security.Cryptography.SHA1.Create();
+ for (int length = 0; length <= 130; length++)
+ {
+ byte[] input = new byte[length];
+ for (int i = 0; i < length; i++)
+ {
+ input[i] = unchecked((byte)(i * 131 + 7));
+ }
+ string expected = ToHex(platformSha1.ComputeHash(input));
+ Assert.That(OneShotHash(input), Is.EqualTo(expected), $"one-shot, length {length}");
+ Assert.That(IncrementalHash(input), Is.EqualTo(expected), $"incremental, length {length}");
+ }
+ }
+
+ [Test]
+ public void StartResetsTheIncrementalState()
+ {
+ var sha1 = default(Sha1ForNonSecretPurposes);
+ sha1.Start();
+ sha1.Append(Encoding.ASCII.GetBytes("garbage that must not leak into the second hash"));
+ byte[] output = new byte[20];
+ sha1.Finish(output);
+
+ sha1.Start();
+ sha1.Append(Encoding.ASCII.GetBytes("abc"));
+ sha1.Finish(output);
+ Assert.That(ToHex(output), Is.EqualTo("a9993e364706816aba3e25717850c26c9cd0d89d"));
+ }
+
+ [Test]
+ public void EcmaStandardPublicKeyYieldsKnownToken()
+ {
+ // The strong-name public key token is defined as the last 8 bytes of the
+ // SHA-1 of the public key, in reversed order. The ECMA standard public key
+ // must produce the well-known token b77a5c561934e089 (mscorlib et al.).
+ byte[] ecmaKey = new byte[16];
+ ecmaKey[8] = 0x04;
+ byte[] hash = new byte[20];
+ Sha1ForNonSecretPurposes.HashData(ecmaKey, hash);
+ string token = ToHex(hash.Skip(12).Reverse().ToArray());
+ Assert.That(token, Is.EqualTo("b77a5c561934e089"));
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
index 800dcc15b..146f9b5a5 100644
--- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
+++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
@@ -499,6 +499,7 @@
+
diff --git a/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs b/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs
index c7ca9ba7f..d59ff406e 100644
--- a/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs
+++ b/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs
@@ -24,7 +24,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
-using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
@@ -273,10 +272,9 @@ namespace ICSharpCode.Decompiler.Metadata
var bytes = Metadata.GetBlobBytes(entry.PublicKeyOrToken);
if ((entry.Flags & AssemblyFlags.PublicKey) != 0)
{
- using (var hasher = SHA1.Create())
- {
- bytes = hasher.ComputeHash(bytes).Skip(12).ToArray();
- }
+ byte[] hash = new byte[20];
+ Sha1ForNonSecretPurposes.HashData(bytes, hash);
+ bytes = hash.Skip(12).ToArray();
}
publicKeyToken = bytes;
diff --git a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs
index 5155f428e..431f089d0 100644
--- a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs
+++ b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs
@@ -24,7 +24,6 @@ using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
-using System.Security.Cryptography;
using System.Text;
using ICSharpCode.Decompiler.TypeSystem;
@@ -40,12 +39,10 @@ namespace ICSharpCode.Decompiler.Metadata
static string CalculatePublicKeyToken(BlobHandle blob, MetadataReader reader)
{
// Calculate public key token:
- // 1. hash the public key (always use SHA1).
- byte[] publicKeyTokenBytes;
- using (var hasher = SHA1.Create())
- {
- publicKeyTokenBytes = hasher.ComputeHash(reader.GetBlobBytes(blob));
- }
+ // 1. hash the public key (the strong-name format mandates SHA-1; a managed
+ // implementation is used so this works under restrictive crypto policies).
+ byte[] publicKeyTokenBytes = new byte[20];
+ Sha1ForNonSecretPurposes.HashData(reader.GetBlobBytes(blob), publicKeyTokenBytes);
// 2. take the last 8 bytes
// 3. according to Cecil we need to reverse them, other sources did not mention this.
diff --git a/ICSharpCode.Decompiler/Util/BitOperations.cs b/ICSharpCode.Decompiler/Util/BitOperations.cs
index 7edeab21a..53c6756f8 100644
--- a/ICSharpCode.Decompiler/Util/BitOperations.cs
+++ b/ICSharpCode.Decompiler/Util/BitOperations.cs
@@ -7,6 +7,10 @@ namespace System.Numerics
{
internal static class BitOperations
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint RotateLeft(uint value, int offset)
+ => (value << offset) | (value >> (32 - offset));
+
private static ReadOnlySpan TrailingZeroCountDeBruijn => new byte[32]
{
00, 01, 28, 02, 29, 14, 24, 03,
diff --git a/ICSharpCode.Decompiler/Util/Sha1ForNonSecretPurposes.cs b/ICSharpCode.Decompiler/Util/Sha1ForNonSecretPurposes.cs
new file mode 100644
index 000000000..fed5d6d01
--- /dev/null
+++ b/ICSharpCode.Decompiler/Util/Sha1ForNonSecretPurposes.cs
@@ -0,0 +1,240 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Vendored from dotnet/runtime, src/libraries/Common/src/System/Sha1ForNonSecretPurposes.cs
+// (commit 6e51f762bc4c98ea90ae6ca21c4e220b4b2e7a5c). The only changes are the "unchecked" blocks
+// in Finish and Drain: upstream compiles without overflow checks, while this project sets
+// CheckForOverflowUnderflow, and SHA-1 relies on wrapping 32-bit arithmetic.
+//
+// Strong-name public-key tokens are defined by ECMA-335 as a SHA-1 digest, so this managed
+// implementation is used instead of System.Security.Cryptography.SHA1 to keep reading assembly
+// metadata independent of the host crypto policy (FIPS-mode systems refuse to create platform
+// SHA-1 instances).
+
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.Numerics;
+
+namespace System
+{
+ ///
+ /// Implements the SHA1 hashing algorithm. Note that
+ /// implementation is for hashing public information. Do not
+ /// use code to hash private data, as implementation does
+ /// not take any steps to avoid information disclosure.
+ ///
+ internal struct Sha1ForNonSecretPurposes
+ {
+ private long _length; // Total message length in bits
+ private uint[] _w; // Workspace
+ private int _pos; // Length of current chunk in bytes
+
+ ///
+ /// Computes the SHA1 hash of the provided data.
+ ///
+ /// The data to hash.
+ /// The buffer to receive the hash value.
+ public static unsafe void HashData(ReadOnlySpan source, Span destination)
+ {
+ Debug.Assert(destination.Length == 20);
+
+ Span w = stackalloc uint[85];
+
+ Start(w);
+
+ int originalLength = source.Length;
+
+ while (source.Length >= 64)
+ {
+ for (int i = 0; i < 16; i++)
+ {
+ w[i] = BinaryPrimitives.ReadUInt32BigEndian(source);
+ source = source.Slice(4);
+ }
+ Drain(w);
+ }
+
+ Span tail = stackalloc byte[2 * 64];
+ source.CopyTo(tail);
+ int pos = source.Length;
+ tail[pos++] = 0x80;
+ while ((pos & 63) != 56)
+ {
+ tail[pos++] = 0x00;
+ }
+ BinaryPrimitives.WriteUInt64BigEndian(tail.Slice(pos), (ulong)originalLength * 8);
+ tail = tail.Slice(0, pos + 8);
+
+ while (tail.Length > 0)
+ {
+ for (int i = 0; i < 16; i++)
+ {
+ w[i] = BinaryPrimitives.ReadUInt32BigEndian(tail);
+ tail = tail.Slice(4);
+ }
+ Drain(w);
+ }
+
+ for (int i = 80; i < w.Length; i++)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(destination, w[i]);
+ destination = destination.Slice(4);
+ }
+ }
+
+ ///
+ /// Call Start() to initialize the hash object.
+ ///
+ public void Start()
+ {
+ Start(_w ??= new uint[85]);
+
+ _length = 0;
+ _pos = 0;
+ }
+
+ private static void Start(Span w)
+ {
+ w[80] = 0x67452301;
+ w[81] = 0xEFCDAB89;
+ w[82] = 0x98BADCFE;
+ w[83] = 0x10325476;
+ w[84] = 0xC3D2E1F0;
+ }
+
+ ///
+ /// Adds an input byte to the hash.
+ ///
+ /// Data to include in the hash.
+ public void Append(byte input)
+ {
+ int idx = _pos >> 2;
+ _w[idx] = (_w[idx] << 8) | input;
+ if (64 == ++_pos)
+ {
+ Drain();
+ }
+ }
+
+ ///
+ /// Adds input bytes to the hash.
+ ///
+ ///
+ /// Data to include in the hash. Must not be null.
+ ///
+ public void Append(ReadOnlySpan input)
+ {
+ foreach (byte b in input)
+ {
+ Append(b);
+ }
+ }
+
+ ///
+ /// Retrieves the hash value.
+ /// Note that after calling function, the hash object should
+ /// be considered uninitialized. Subsequent calls to Append or
+ /// Finish will produce useless results. Call Start() to
+ /// reinitialize.
+ ///
+ ///
+ /// Buffer to receive the hash value. Must not be null.
+ /// Up to 20 bytes of hash will be written to the output buffer.
+ /// If the buffer is smaller than 20 bytes, the remaining hash
+ /// bytes will be lost. If the buffer is larger than 20 bytes, the
+ /// rest of the buffer is left unmodified.
+ ///
+ public void Finish(Span output)
+ {
+ Debug.Assert(output.Length == 20);
+
+ long l = _length + 8 * _pos;
+ Append(0x80);
+ while (_pos != 56)
+ {
+ Append(0x00);
+ }
+
+ unchecked
+ {
+ Append((byte)(l >> 56));
+ Append((byte)(l >> 48));
+ Append((byte)(l >> 40));
+ Append((byte)(l >> 32));
+ Append((byte)(l >> 24));
+ Append((byte)(l >> 16));
+ Append((byte)(l >> 8));
+ Append((byte)l);
+ }
+
+ for (int i = 80; i < _w.Length; i++)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(output, _w[i]);
+ output = output.Slice(4);
+ }
+ }
+
+ ///
+ /// Called when pos reaches 64.
+ ///
+ private void Drain()
+ {
+ Drain(_w);
+ _length += 512; // 64 bytes == 512 bits
+ _pos = 0;
+ }
+
+ private static void Drain(Span w)
+ {
+ unchecked
+ {
+ var _ = w[84]; // Hint to eliminate bounds checks
+
+ for (int i = 16; i < 80; i++)
+ {
+ w[i] = BitOperations.RotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
+ }
+
+ uint a = w[80];
+ uint b = w[81];
+ uint c = w[82];
+ uint d = w[83];
+ uint e = w[84];
+
+ for (int i = 0; i < 20; i++)
+ {
+ const uint k = 0x5A827999;
+ uint f = (b & c) | ((~b) & d);
+ uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp;
+ }
+
+ for (int i = 20; i < 40; i++)
+ {
+ uint f = b ^ c ^ d;
+ const uint k = 0x6ED9EBA1;
+ uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp;
+ }
+
+ for (int i = 40; i < 60; i++)
+ {
+ uint f = (b & c) | (b & d) | (c & d);
+ const uint k = 0x8F1BBCDC;
+ uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp;
+ }
+
+ for (int i = 60; i < 80; i++)
+ {
+ uint f = b ^ c ^ d;
+ const uint k = 0xCA62C1D6;
+ uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp;
+ }
+
+ w[80] += a;
+ w[81] += b;
+ w[82] += c;
+ w[83] += d;
+ w[84] += e;
+ }
+ }
+ }
+}